Examples
Complete, copy-pasteable code for the common shapes, a plain component, control flow, a stateful component with a controller, and a graph of components that load lazily. Everything here is runnable: the same set is a live, unbundled demo in the reference repository.
Illustrative · pairs with the runnable proof of conceptA minimal component
One prop, one native root, a scoped style. It lowers to a real <button>, no wrapper, no shadow root, with a data-component provenance stamp.
<!-- button.html — a minimal component: one prop, one native root, scoped style. -->
<template component="x-button">
<defs>
<prop name="variant" type="outline | solid" default="outline">Visual treatment.</prop>
</defs>
<button :data-variant="variant"><slot></slot></button>
<style>
button { font: inherit; }
button[data-variant="solid"] { background: CanvasText; color: Canvas; }
</style>
</template>
<!-- use -->
<x-button variant="solid">Save</x-button>
<!-- lowers to -->
<button data-component="x-button" data-variant="solid">Save</button>Control flow, as attributes
Structural $-directives ride on ordinary elements (or a <template>), so they survive restrictive parser contexts like <table> and <select>. Iteration shaping ($where, $sort, $limit, $key) lives on the loop. See Templating.
<!-- templating: a filtered, sorted list and a three-way status, all as directives -->
<ul>
<li $each="p of products" $where="p.inStock" $sort="price,-name" $key="p.id">
<value of="p.price" format="currency" currency="USD"></value> — <span $value="p.name"></span>
</li>
</ul>
<template $match="order.status as s">
<p $when="s = 'pending'">Working…</p>
<p $when="s = 'error'">Something went wrong.</p>
<p $else>Done.</p>
</template>A stateful component, with a controller
The definition is pure data: it declares <state>, exposes a $ref, and names its controller with <link rel="controller">. The behavior lives in a separate ES module that self-registers by tag and drives state; the runtime reflects state to the DOM. See The JavaScript Layer.
<!-- counter.html — local state, and a controller (declared, not contained). -->
<link rel="controller" href="./counter.js">
<template component="x-counter">
<defs>
<prop name="start" type="number" default="0"></prop>
<state name="count" :value="start"></state>
</defs>
<button $ref="btn" type="button">count: <span $value="count"></span></button>
</template>// counter.js — an ordinary ES module that self-registers by tag.
import { defineController } from "html/components";
export const controller = (host) => {
host.refs.btn.addEventListener("click", () => {
// Drive STATE, never the DOM directly. The runtime reflects count to the <span>.
host.state.count = host.state.count + 1;
});
};
// Explicit registration, shaped like customElements.define(tag, class).
defineController("x-counter", controller);The same shape wraps any imperative library: reach the element with a $ref, let the library own that (unbound) subtree, and re-run on data changes with host.effect.
<!-- chart.html — wraps a foreign drawing library through a $ref to a canvas. -->
<link rel="controller" href="./chart.js">
<template component="x-chart">
<defs>
<state name="bars" :value="[3, 7, 2, 5, 8, 4]"></state>
</defs>
<figure>
<canvas $ref="surface" width="260" height="90" role="img" aria-label="chart"></canvas>
</figure>
</template>// chart.js — the library owns its own (unbound) canvas; an effect re-runs if data changes.
import { defineController } from "html/components";
import { Chart } from "chart-lib"; // a bare specifier; the import map resolves it
defineController("x-chart", (host) => {
const chart = new Chart(host.refs.surface, { data: host.state.bars });
host.effect(() => chart.update(host.state.bars));
host.on("disconnect", () => chart.destroy());
});A graph of components
A component that uses others declares only its own dependencies. The page resolves just the entry; everything below is discovered from the definitions, and each controller is imported lazily on first connect. See the composition model.
<!-- app.html — purely declarative composition. It declares ONLY the components it uses;
the graph composes transitively, like an ES-module graph. No controller, no <script>. -->
<link rel="component" href="./counter.html">
<link rel="component" href="./chart.html">
<template component="x-app">
<defs>
<prop name="title" type="string" default="App"></prop>
</defs>
<main>
<h1 $value="title"></h1>
<x-counter start="3"></x-counter>
<x-chart></x-chart>
</main>
</template><!doctype html>
<link rel="component" href="/components/app.html"> <!-- resolve only the ENTRY component -->
<script type="module" src="/htmlnext.js"></script> <!-- the runtime: one module -->
<x-app title="Dashboard"></x-app>Run it yourself
This exact example, a base index.html, component definitions as .html files, controllers as .js modules, and a small runtime, is a complete, unbundled, runnable demo in the reference implementation at examples/poc/ in the html repository. Serve that directory with any static server and open it: the counter increments and the chart draws, with no build step.
The demo proves composition, tag-based registration, and lazy controller loading end to end. It is deliberately simplified in places (eager definition loading, coarse reactivity, no SSR); its README says exactly what is demonstrated versus deferred.