Author once. Use native DOM, Vue, React, or Svelte.
This pnpm monorepo holds the JavaScript tools for the HTML Next proposals.
@nextwebwg/html-next supplies component tooling; HTMLKit builds applications on it, and the
converter and unplugin adapt it to other build workflows.
All four packages share one version and publish together. Shared policy and verification live at the
repository root.
Use html-next-check for
compiler diagnostics without build output, alongside TypeScript or your framework's typechecker.
The command supports native, Vue, React, and Svelte targets and JSON output for tooling.
| Package | Role | Current scope |
|---|---|---|
@nextwebwg/html-next |
The tools | Live browser runtime, the shared compiler, validity on any element, native form request construction, and native-DOM/CSS/package generation |
@nextwebwg/html-next-unplugin |
Bundler adapter | Closed-graph unplugin and Vite application/library builds |
@nextwebwg/html-next-converter |
Framework adapter | Vue, React, and Svelte conversion |
@nextwebwg/htmlkit |
Application platform | File-based and registered routes, layouts, server loaders, dev/build/preview, and static deployment |
The tools package implements both proposals it needs: Declarative HTML Components and HTML Forms. A component is authored once as inert, browser-parseable HTML; the same definition can run directly in a browser or compile to native DOM, CSS, types, and inspectable package artifacts. The component language is defined by the proposal; this repository is its JavaScript tooling, verified by conformance tests.
HTML Forms lives at @nextwebwg/html-next/forms and operates
on native HTMLFormElement and submitter objects. It has no component, template, or
reactive-runtime dependency, so importing that subpath pulls in nothing else; the component runtime
consumes its validity model for form declarations but does not re-export request construction.
Stage 0: the syntax and generated package shape may change. The repository and its packages are MIT-licensed and publish
1.0.0-alphaprereleases to npm under the defaultlatesttag. Any alpha may break a component; Versions and stability explains what 1.0 will promise and how a library declares the versions it supports.
The Declarative Components implementation supports the same component language in three delivery modes:
| Mode | Package | Input | Output |
|---|---|---|---|
| Live browser runtime — supports any graph | @nextwebwg/html-next |
Any component graph selected or added by the application at runtime | One distributable that parses, mounts, updates, and disconnects every supported capability, for any graph, with no build step |
| Compiled native build — tree-shaken, via a Vite unplugin | @nextwebwg/html-next-unplugin |
An application entry graph or a concrete set of library entries | Native DOM modules tree-shaken to the exact capabilities the graph uses, with shared support combined by the bundler |
| Framework conversion — to Vue, React, or Svelte | @nextwebwg/html-next-converter |
A component graph plus a target framework | Vue or Svelte single-file components, or React TSX components, with no HTML Next runtime dependency |
These are the only three build outputs, and they are distinct: the runtime ships one universal distributable, the compiled build emits tree-shaken native DOM for a known graph, and the converter generates framework source from Declarative Components. The converter is one-way output; it is not an ingest that imports Shadow DOM or any other format into Declarative Components. Such migrations are consumer-specific and live outside this repository.
An application build may serve as a complete alternative to a framework application. A library build keeps independently consumable component entries while allowing the consumer's bundler to combine their shared support. All three modes consume one normalized semantic model and must preserve appearance, interactions, state, events, validation, lifecycle, and successful hydration. Optional language extensions are the exception: each lists the modes that build it in Versions and stability. Frameworks may use their own DOM and SSR representations; acceptance is based on how the resulting component looks and acts, including controller connect/disconnect and cleanup.
The detailed contracts and independent progress tracks are in the proposal and delivery goal ledger.
Looma is a stack-agnostic UI library built on Declarative
Components. Every Looma component is authored once as a definition in this component language;
@threadlabs/looma registers those definitions with the live runtime for HTML pages, and its Vue
entry point ships components generated by the converter. It depends on @nextwebwg/html-next
directly, so its component corpus — declared props, controllers, slots, scoped styles, and
structural directives — exercises both the runtime and the Vue target outside this repository.
HTML Next ranked #3 of 15 in a six-workload reactive-primitive benchmark on Node 24/macOS ARM64. The results and reproduction guide includes raw timings, exclusions, measurement limits and clean-checkout commands. Both the full matrix and the smaller regression comparison are manual tools for performance work.
Use Node 22.22.2+ or Node 24.15+ and pnpm through Corepack:
corepack pnpm install --frozen-lockfile
corepack pnpm verify:pr
corepack pnpm test:browser
corepack pnpm test:targets
corepack pnpm test:consumerPlaywright's pinned Chromium, Firefox, and WebKit builds are required for the browser
gates. Install them once with corepack pnpm exec playwright install chromium firefox webkit.
CI runs Node tests, browser regressions and installed-consumer checks only for changed packages and their transitive workspace consumers. Core changes affect all four packages; converter changes also affect the Vite plugin; HTMLKit changes stay with HTMLKit. Shared toolchain changes select all packages. Documentation changes run repository contracts without package suites. Pure shared-version changes do not widen functional test selection. Release metadata, generated output and installed consumers for the changed package versions are still verified.
The routine target is five minutes on the parallel critical path, including setup. Browser batches
target three minutes of tests, and long specs are split by engine and delivery mode. The automatic
verification and publication path has a fifteen-minute execution budget: one minute for selection,
ten for parallel checks, one for the Required result, and three for publication. GitHub runner queue time is outside that budget.
Windows/macOS and the full Node LTS matrix use the manual Cross-platform workflow. Rendering
benchmarks use the manual Framework rendering workflow, and
pnpm verify:performance --base=origin/main runs explicitly for reactive performance work.
The manual Converter corpus workflow runs the broad React, Vue and Svelte public-corpus sweeps
for shared compiler changes or parity investigations. Focused framework regressions remain automatic
in Chromium, Firefox and WebKit.
Deno/Bun installed-consumer smoke checks run only when core or HTMLKit changes affect HTMLKit.
Automatic npm publication waits for successful CI on the exact main commit and has its own
three-minute cap. See release mechanics.
The carrier declares the public interface, its optional controller, and one semantic root. The controller is an ordinary ES module with a default export; it is part of the component dependency graph, not a registration script.
<template component="x-counter" controller="./counter.js"
status="early" summary="A native counter button.">
<defs>
<state name="count" type="number" value="0"></state>
<computed name="label" from="concat('Count: ', $count)"></computed>
</defs>
<button $ref="button" type="button">
<span>{$label}</span>
</button>
<style>
button { font: inherit; }
button:invalid { outline: 2px solid red; }
</style>
</template>// counter.js
export default function controller({ refs, state }) {
const increment = () => { state.count += 1; };
refs.button.addEventListener("click", increment);
return () => refs.button.removeEventListener("click", increment);
}Definitions may also use declarative handlers, structural directives, two-way bindings, named and data-derived slots, typed data sources, native form participation, and generalized validation. See the proposal for the complete syntax.
One script in the <head> is the whole setup. The browser entry starts itself, loads every
component the page links, and follows each definition's declared component and controller
dependencies:
<head>
<script type="module" src="https://cdn.jsdelivr.net/npm/@nextwebwg/html-next/dist/browser.js"></script>
<link rel="component" href="/components/app.html">
</head>
<body>
<x-app></x-app>
</body>The runtime keeps one MutationObserver on the document. A <link rel="component"> added
later loads its graph into the running page, and instances of any registered tag are rendered
as they are added, including ones that were waiting for their definition. HTMLNext.ready
resolves once the initially linked components have loaded. A compiled build does none of this:
it is closed over the components it was built from.
A same-origin href needs nothing else. A bare href such as @acme/ui/app.html is a package
specifier that the page's import map resolves, and a component root on another origin needs an
import-map entry. Relative HTML and controller edges must stay inside their root. Definitions are
parsed as inert data and cannot add import maps, scripts, base URLs, or policy metadata.
Controller modules are trusted same-realm JavaScript: native ESM, CORS, and CSP govern their
module graph, but ESM is not a sandbox.
Component resources may also contain titles, non-policy-changing metadata, and ordinary metadata
links outside their component carriers. The Node and browser graph loaders accept and ignore these
nodes: they do not change the consuming document's head, evaluate metadata bindings, or fetch linked
stylesheets and other assets. Application tooling may interpret them separately. This allowance
does not admit arbitrary resource-level nodes: <style>, every <script> type, <base>,
http-equiv metadata, HTML Imports, body elements, plain non-component templates, and
non-whitespace text remain rejected, as do executable event-handler attributes. Scoped <style>
inside a component carrier and its declared controller module keep their existing behavior.
The runnable live graph example uses this entry.
The live loader supplies the proposal behavior and browser compatibility needed by the definitions it loads:
| Surface | Runtime behavior |
|---|---|
| Component discovery and lifecycle | One shared MutationObserver discovers registered component tags and balances connection cleanup for lowered roots. |
| Component parsing | The browser's HTML parser creates the inert DOM; the library reads declarations, validates the proposal grammar, and reports component diagnostics. |
| Reactive declarations | Native events and microtasks drive a small dependency layer for live state, computed values, bindings, and effects. |
| Declared types | Component-authored prop and event types are parsed and enforced at their public boundaries; external data may use an application adapter. |
Dynamic $html |
The HTML fragment parser plus the Sanitizer API's safe-default allowlist produces deterministic output across browsers and SSR. Native setHTML() is deliberately not used: Firefox currently parses malformed table content differently, which would break hydration parity. |
| Scoped styles | Native @scope provides the boundary; selector transformation preserves lowered component roots, nested components, and projected content. |
| Keyed lists | Native DOM identity and moveBefore() preserve retained blocks where available; the WebKit compatibility path uses insertBefore(), with the same keyed reconciliation. |
| Component resources | Native URL, Fetch, ESM, CORS, and CSP provide loading primitives; the loader applies the proposal's component graph and trust-root rules. |
The live runtime requires native CSS @scope support (Chrome 118+, Safari 17.4+, and Firefox
146+). Ahead-of-time generated targets retain provenance-attribute scoping for older browsers.
The native build currently specializes static markup, basic reactivity, numeric-computed state, and scalar props. CI records zero live-parser and full-runtime contribution for those four capability fixtures. Keyed lists, declared reads, and controller lifecycle currently use the general runtime fallback. These fixtures attribute feature cost; the build product operates on an application or library graph and should share its required support across that graph. The measured inventory and owner decisions live in the native runtime audit.
The current public loader is one predictable bundle. A future packaging experiment may split compatibility features into progressively loaded modules selected by browser capability and authored syntax; that is a potential delivery optimization, not current behavior.
The CLI reads component HTML and static ESM imports without executing controllers:
html-next check components/app.html
html-next inspect components/app.html
html-next build components/app.html --out-dir generated
html-next build components/app.html --out-dir generated --target vue --target stylesWhen working from this repository, substitute
corepack pnpm exec tsx packages/html-next/src/cli.ts for html-next.
inspect reports component, controller, and transitive module edges. build
follows the complete graph and emits deterministic artifacts plus html.manifest.json,
which is a build inventory—not a second component contract.
Generated targets preserve the definition's native root; they do not add a component wrapper. The checked-in button output demonstrates each target.
The @nextwebwg/html-next/server entry renders validated definitions with the same
general runtime used in the browser:
import { parseComponent } from "@nextwebwg/html-next";
import { renderComponents } from "@nextwebwg/html-next/server";
const definition = parseComponent(componentSource);
const { html, css } = await renderComponents('<x-counter id="counter"></x-counter>', {
definitions: [definition],
state: { "#counter": { count: 5 } },
});Serve the returned markup and styles. In the browser, register the same definitions through
registerComponentDefinitions() and call lowerDocument() or observeDocument() from the runtime
entry. Hydration restores props, explicitness, declared state and projected slots while adopting
existing native nodes. Node-to-browser tests compare the restored instance and subsequent updates
against fresh client rendering in Chromium, Firefox and WebKit.
See Node rendering and hydration for the API, platform choices and verification. The server renders the declarative baseline; browser hydration connects declared reads and attaches controllers through the live loader or bundled controller imports, following the proposal's lifecycle. Tests cover both the live loader and a tree-shaken browser bundle, including later controller and read updates. The specialized build and converter delivery tracks have separate completion criteria.
HTML Next has a fully specified type grammar rather than a loose “CSS-like” shorthand. It covers scalar, keyword, collection, structured, nullable, web-value, callback, opaque, and trusted-content forms, including source diagnostics and TypeScript projections.
Native form controls keep the browser's Constraint Validation API. Managed ordinary elements
receive the same validity shape and invalid events from a small pure validator whose supported
constraints are checked against native controls in Chromium, Firefox, and WebKit. This preserves
browser behavior without creating and configuring a detached control for every validation.
Authors write ordinary :valid, :invalid, and :user-invalid selectors; the runtime and
generated CSS carry the compatibility rewrite for browsers that cannot apply those pseudo-classes
to arbitrary elements. Runtime size and speed changes follow the
performance guardrails.
The package assembler emits:
- side-effect registration and concrete component HTML;
- Vanilla and Vue components with native roots;
- typed props (as HTML attributes), events, slots, and controller subscriptions;
- scoped component CSS;
- controller and dependency graphs preserved as static modules; and
- explicitly declared ordinary JavaScript, declaration, and CSS pass-through exports.
Installed packages resolve through normal package exports and can be bundled without a browser import map. Live URLs and installed packages use the same component definitions; only application resolution and trust differ.
- Proposal (the source of truth; not in this repository)
- Tools guide, published from
docs/guide - Converter requirements
- Conformance corpus
- Style-scoping note
- Historical component-generation plan
- Looma, a UI library built on this implementation
The public HTML Next Working Draft explains and motivates the proposal. This repository and its conformance corpus are library-agnostic.