Examples

Components, end‑to‑end.

Each example below is a real custom element rendered on this page. The code shown is the same code that's running. Inspect any of them in DevTools to see the Shadow DOM tree.

01 — <ex-counter>

Reactive counter

State held in a closure inside afterMount. Listeners query the shadow root, not the document.

import { describe, build } from "@ra9/tan-compose"; build("ex-counter", describe({ styles: { display: "inline-flex", flexDirection: "column", alignItems: "center", gap: "14px" }, template: ` <div class="display">0</div> <div class="ctrls"> <button class="dec">−</button> <button class="rst">reset</button> <button class="inc">+</button> </div> `, afterMount: function () { const root = this.shadowRoot; const display = root.querySelector(".display"); let count = 0; const sync = () => { display.textContent = String(count); }; root.querySelector(".dec").addEventListener("click", () => { count--; sync(); }); root.querySelector(".inc").addEventListener("click", () => { count++; sync(); }); root.querySelector(".rst").addEventListener("click", () => { count = 0; sync(); }); }, }));
02 — <ex-button-row>

Themed button row

Children defined declaratively. Each click handler is registered and torn down for you.

build("ex-button-row", describe({ styles: { display: "flex", gap: "10px", flexWrap: "wrap", justifyContent: "center" }, children: [ describe({ tag: "button", template: "Primary", action: () => alert("primary"), styles: { padding: "10px 18px", background: "#14171f", color: "#fff", border: "none", borderRadius: "8px", cursor: "pointer" } }), describe({ tag: "button", template: "Accent", action: () => alert("accent"), styles: { padding: "10px 18px", background: "#a16939", color: "#fff", border: "none", borderRadius: "8px", cursor: "pointer" } }), describe({ tag: "button", template: "Ghost", action: () => alert("ghost"), styles: { padding: "10px 18px", background: "transparent", color: "#14171f", border: "1px solid #d9cfb8", borderRadius: "8px", cursor: "pointer" } }), ], }));
03 — <ex-card>

Composed card with theme variables

theme exposes CSS custom properties on :host. Children reference them via var(--name).

build("ex-card", describe({ theme: { surface: "#ffffff", border: "#ece5d3", ink: "#14171f", accent: "#a16939" }, styles: { display: "block", background: "var(--tc-color-surface, #ffffff)", border: "1px solid var(--border)", borderRadius: "12px", padding: "22px", maxWidth: "420px" }, children: [ describe({ tag: "h3", template: "Theme variables", styles: { color: "var(--tc-color-ink, #14171f)", margin: "0 0 8px" } }), describe({ tag: "p", template: "Defined once on the host, used anywhere inside.", styles: { color: "#5a6072", margin: "0 0 14px", lineHeight: "1.55" } }), describe({ tag: "a", template: "Read more →", attributes: { href: "./docs.html" }, styles: { color: "var(--tc-color-accent, #a16939)", fontWeight: "600", textDecoration: "none" } }), ], }));
04 — <ex-toggle>

Toggle with observed attributes

Declare observedAttributes and the component re‑renders whenever a listed attribute changes — even from outside the component.

build("ex-toggle", describe({ observedAttributes: ["data-state"], styles: { display: "inline-flex", alignItems: "center", gap: "10px", fontFamily: "'JetBrains Mono', monospace" }, template: `<span class="dot"></span><span class="label">off</span>`, afterMount: function () { paint(this); }, })); // Re-render fires on attribute change; we re-paint after each render. // (See afterMount + observedAttributes in the docs.)
05 — <ex-pinger>

Custom event emitter

emitEvent() dispatches a bubbling, composed CustomEvent. The page listens at the document level.

waiting for ping…
build("ex-pinger", describe({ tag: "button", template: "send ping", styles: { padding: "10px 18px", background: "#a16939", color: "#fff", border: "none", borderRadius: "8px", cursor: "pointer", fontWeight: "500" }, action: function () { this.emitEvent("ping", { at: Date.now() }); }, })); document.addEventListener("ping", (e) => { document.getElementById("ping-log").textContent = `pinged at ${new Date(e.detail.at).toLocaleTimeString()}`; });