Reference

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 concept

A 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
<!-- 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 data: it declares <state>, exposes a $ref, and names the controller it requests with <link rel="controller" href>. That link does not execute code. Only the application's matching html-next-controller/tag import-map entry authorizes the ES module. The approved module self-registers by tag and drives state; the runtime reflects state to the DOM. See The JavaScript Layer.

counter.html
<!-- counter.html — local state plus a controller request; this link does not execute it. -->
<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
// 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);
Integrating a foreign library

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
<!-- 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
// 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 data dependencies. The page resolves just the entry, so definitions below it are discovered transitively; the page separately authorizes the finite controller set, and each approved controller is imported lazily on first connect. See the composition model.

app.html
<!-- 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>
index.html
<!doctype html>
<link rel="component" href="/components/app.html">   <!-- resolve only the ENTRY component -->
<script type="importmap">                            <!-- only the APPLICATION authorizes code -->
{
  "imports": {
    "html-next-controller/x-counter": "/components/counter.js",
    "html-next-controller/x-chart": "/components/chart.js",
    "chart-lib": "/vendor/chart-lib.js"
  },
  "integrity": {
    "/components/counter.js": "sha384-…",
    "/components/chart.js": "sha384-…",
    "/vendor/chart-lib.js": "sha384-…"
  }
}
</script>
<script type="module" src="/htmlnext.js" integrity="sha384-…"></script>

<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.

Proof of concept, honestly scoped

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.