npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

aktion-runtime

v0.5.15

Published

Aktion is a single web component that turns a compact, streaming-first DSL into a rich, interactive UI inside its shadow DOM. Works in React, Vue, Angular, Svelte, plain HTML — or no framework at all.

Readme

aktion

License: MIT Docs PRs welcome

A framework-agnostic web component that renders LLM-generated UI from Aktion — a reactive language whose surface syntax is a strict subset of JavaScript, designed for chat assistants. Drop one <script> tag and one <aktion-app> tag into any HTML page and you have a streaming, interactive renderer for an LLM's response.

<script type="module" src="https://asfand-dev.github.io/aktion/dist/aktion.js"></script>
<aktion-app theme="light">
  $app(Card([
    CardHeader("Hello", { subtitle: "Generative UI in plain HTML" }),
    Markdown("This card was streamed in as **plain text**.")
  ]))
</aktion-app>

That is the whole integration. Works in React, Vue, Angular, Svelte, plain HTML, or no framework at all.


Table of contents


What's in the box

Everything you need at runtime ships in a single bundle:

  • A streaming-first parser. Line-oriented, error-tolerant. Each statement commits to the DOM as soon as it arrives. The surface syntax is a strict subset of JavaScriptfunction declarations, for...of, if/else, switch/case, template literals with ${expression} interpolation, arrow functions, default parameters, destructuring, spread, optional chaining (a?.b), nullish coalescing (a ?? b), and object-literal named arguments. Every Aktion program is valid JavaScript.
  • One reactive atom kind. Declare any reactive state with $name = value and read or write it with $name. The $ prefix is the only thing that makes a binding reactive — let / const / var keywords are optional and have no effect on reactivity. The runtime tracks dependencies automatically — and at path granularity: reading $user.name subscribes to user.name alone, so a write to $user.role never re-renders, recomputes, or re-fires an effect that only read name (see Fine-grained reactivity below). Automatic two-way binding via direct state refs (and member chains rooted at one — value: $form.email), and a $util runtime namespace of pure helpers ($util.filter, $util.sort, $util.find, $util.groupBy, $util.format, $util.formatDate, $util.plural, $util.case, $util.range, $util.pick, $util.omit, $util.merge, $util.cloneDeep, $util.chunk, $util.partition, $util.keyBy, …) callable from Aktion expressions and ordinary JavaScript alike.
  • One component-call shape. Every call follows the trailing-object rule — Component(positionalArg, { prop: value, … }). At most one positional argument; every other argument goes in a trailing { } object literal.
  • One HTTP primitive. $http({ url, method, headers, body, query, ... }) is the only network call. Each call is self-contained (pass a full absolute url; GET is the default; no host-wide defaults). It returns a reactive resource bag exposing data | error | status | loading | headers | lastUpdated, plus the callables refetch() and cancel(). Re-run a request via refetch() or by wrapping it in an $effect(..., [$dep]).
  • $storage and $console globals. Always in scope, no import, lowercase. $storage.set/get (localStorage by default), $storage.session.*, $storage.cookies.* with object-literal options, and console.log/error/warn/info/debug.
  • $toast — imperative notifications. A reserved namespace that owns the toast lifecycle so you never hand-manage a $toasts = [...] array. $toast.show(message, { tone?, title?, duration? }) (auto-dismisses after duration ms, default 4000; 0 keeps it), plus shortcuts $toast.success/.error/.info/.warning, $toast.dismiss(id), and $toast.clear(). Toasts render themselves (stacked top-right) — no Toasts(...) to wire into $app. For custom placement, render the reactive $toast.items list yourself with Toasts/Toast (which opts out of the auto-rendered layer).
  • $form({ values, rules, onSubmit }) — the form engine. Managed reactive form with per-field $util.rules validators (including async ones via $util.rules.asyncCustom — think server-side uniqueness checks), touched/dirty tracking, and form.submit() (alias handleSubmit()) that validates — awaiting async rules — then calls onSubmit. Access form.values.field (two-way bindable), form.errors.field, form.touched.field, form.dirty (flips on the first edit, even via two-way binding; clears on reset), form.valid, form.submitting (stays true until an async onSubmit settles), form.validating (async rules in flight), and call form.setField(), form.touch(), form.reset(). Input/TextArea/Select/NumberInput accept onBlur/onFocus, so validate-on-blur is one prop: Input("email", { value: form.values.email, onBlur: () => form.touch("email") }).
  • $store persistence + undo/redo. Add persist: "key" to mirror the store to localStorage (or persistIn: "session" for sessionStorage) — hydrates on mount. Add history: true (or a depth number) for full undo/redo: store.undo() / store.redo() / store.canUndo / store.canRedo.
  • Universal sx / animate styling channel. Every component accepts a token-aware sx object — spacing (p px py pt…, logical ps pe ms me; px/mx emit padding-inline/margin-inline so RTL apps mirror automatically), sizing (w h minW maxW…), color (bg color borderColor, gradient refs like "gradient.brand"), surface (border radius shadow opacity backdrop), background imagery (bgImage + bgOverlay wash + bgSize), typography (fontSize weight textDecoration textAlign), flex/grid (display direction align justify wrap grow shrink basis columns), position/layering (position top right bottom left inset zIndex — layer tokens resolve through themeable --rui-z-* vars), and interaction states: { hover|focus|active|disabled|… } compiled to scoped CSS rules — plus animate: "fade-up" motion presets. Any value accepts a { base, sm, md, lg, xl } map that resolves to real @media breakpoints. No stylesheet required.
  • 60+ new components since v0.4: marketing bands (Section, Split, Bento), motion (Reveal, Transition, FlipList, Parallax), accessibility primitives (VisuallyHidden, SkipLink, LiveRegion, FocusTrap), realtime (TypingIndicator, PresenceAvatars, ReactionPicker, LiveCursor), e-commerce (Cart, ProductCard, OrderSummary), canvas (DrawingCanvas, SignaturePad), scheduling (Calendar), virtualization (VirtualGrid), and many more.
  • RTL + logical layout. Set dir="rtl" on <aktion-app> and the whole tree flips (text direction, flex order, logical spacing). Programs need no code change.
  • SSR / SSG. renderToString(program, { path, initialState }){ html, state } for server-side rendering. renderToStaticMarkup for static pages.
  • DX tooling. tailwindToSx(classString) maps Tailwind classes to sx; htmlToAktion(html) imports common HTML/JSX; componentSchema() emits a stable JSON schema for editor autocomplete; buildGallery() generates a self-contained component explorer; suggestComponent("Buttn") returns typo candidates.
  • Testing utilities. within(node) for scoped queries, axe(node) for a11y audits from the aktion-runtime/test entry.
  • A React-like DOM reconciler. Diffs each re-render against the live DOM. Text-input value, selection, IME state, scroll positions, <details>.open, and stateful primitives like Tabs are all preserved across renders. Components that need to hold UI state get a helpers.useInstanceState(...) slot keyed by their position in the tree.
  • A rich component library of 275 components spanning layout, forms, charts, data, feedback, navigation, patterns, app-shell composites, editors, advanced UI, motion, marketing, e-commerce, accessibility, realtime, and standard helpers. See Component library.
  • Declarative side effects. $effect(() => { body }, [...deps]) for background work — anonymous blocks where the dependency list mixes state triggers ($atom), lifecycle triggers ("mount", "unmount", "every(N)"), and rate-limit modifiers ("debounce(N)", "throttle(N)"). $effect(() => { … }) with no dependency array is equivalent to $effect(() => { … }, ["mount"]). Declare an effect at the top level for program-wide work, or inside a component function body to scope it to a single instance — timers, watched atoms, and cleanup(fn) registrations tear down when the component leaves the tree. function name(args) { … } (camelCase) declares an action — click-driven mutations that may optionally return a value.
  • Outbound events. $emit("name", { detail }) dispatches a CustomEvent on the host element from inside any action / effect / lambda body. The host listens with el.addEventListener("name", …).
  • A built-in router. pages = $router({ "/path": Component(), "/users/:id": UserPage({ id: params.id }), default: NotFound() }) plus NavLink(label, { to }) and a reserved route handle that exposes route.path, route.params, route.query, route.pattern, and route.navigate("/path"). Hash-based, framework-agnostic, always wired up.
  • Six built-in themes (light, dark, corporate, soft, glass, modern) plus full custom-token support via CSS custom properties. 80+ design tokens organised into colors, radius, font, spacing, shadows, gradients (referenced as "gradient.brand" from sx/GradientText), zIndex (layer tokens feeding sx.zIndex), and motion groups — plus fonts (Google-Fonts shorthand import) and icons (custom inline-SVG registration). Brand the UI from inside the script with a $theme({...}) statement.
  • $i18n factory. const { t, setCurrentLanguage, getCurrentLanguage } = $i18n({ defaultLanguage, currentLanguage, translations }) builds a translation bundle keyed by language, with {name} placeholder interpolation.
  • Font Awesome 6.7.2 auto-loaded — every icon prop accepts a Free Font Awesome name (no fa- prefix). Use Icon(name, { variant?, size? }) for standalone glyphs. Variant prefixes supported: "regular:star", "brands:github".
  • Markup escape hatches. HTMLTag(tag, { attributes?, children? }) and Styles(css) are the last-resort raw-HTML / raw-CSS injectors when no standard component captures the design.
  • Third-party widget interop. Mount({ setup, update?, cleanup?, props? }) hosts an imperative library (chart, map, editor, payment element) with a managed lifecycle; WebComponent(tag, { attributes?, properties?, on? }) bridges native custom elements; $script({ src, global? }) loads an external SDK once; and the $dom namespace gives auto-disposed resize / intersection / mutation observers + $dom.measure(node).
  • Document head & SEO. $head({ title, meta, og, twitter, link, jsonLd }) is a reactive head manager — per-route titles / meta / Open Graph / JSON-LD that also feed renderToString (head + headAttrs) for crawlable SSR.
  • A system prompt generator. Emits a clean, ordered prompt teaching the LLM exactly which components, builtins, and tools are available. Two flavours ship: system_prompt.txt (full — every feature) and system_prompt_chat.txt (compact — read-only UI conversion).
  • Host-side tooling. A canonical formatter, structured-edit delta protocol, AST inspector, and LSP-ready language service all exported from aktion/tooling.

Everything lives inside a Shadow DOM, so the renderer's styles never leak into your application — and your application's styles never leak into the renderer.


Quick start

1. Load the bundle

Use the CDN build (no install, just a script tag):

<script type="module" src="https://asfand-dev.github.io/aktion/dist/aktion.js"></script>

For non-module setups (older bundlers, embedded contexts) use the IIFE build:

<script src="https://asfand-dev.github.io/aktion/dist/aktion.iife.js" defer></script>

…or install from npm and import once from your client-side entry point:

npm install aktion-runtime
# yarn add aktion-runtime
# pnpm add aktion-runtime
import "aktion-runtime";

The package is published as aktion-runtime. The npm tarball ships only the compiled dist/ output (ESM + CJS + UMD + IIFE bundles, type declarations, the stylesheet, and the two system_prompt*.txt files), so installs stay small. Subpath imports are available for convenience:

import "aktion-runtime/style.css";
const SYSTEM_PROMPT = await fetch(
  new URL("aktion-runtime/system_prompt.txt", import.meta.url),
).then((r) => r.text());

The CSS is bundled inside the JS and injected into each instance's shadow root, so you do not need a separate stylesheet.

2. Mount the tag

<aktion-app id="reply" theme="light"></aktion-app>

3. Render a response

Three equivalent ways:

<!-- as an attribute -->
<aktion-app response='$app(Card([CardHeader("Hi")]))'></aktion-app>

<!-- as inner text (rendered on connect) -->
<aktion-app>
  $app(Card([CardHeader("Hi")]))
</aktion-app>

<!-- from an external file (linked with its imports) -->
<aktion-app src="./app.aktion"></aktion-app>

<!-- as a property/method -->
<script>
  const el = document.querySelector("aktion-app");
  el.setResponse(`
    $app(Column([greeting]))
    greeting = Card([CardHeader("Hello", { subtitle: "Generative UI in plain HTML" })])
  `);
</script>

4. Stream from your LLM

const response = await fetch("/api/chat", {
  method: "POST",
  body: JSON.stringify({ system: systemPrompt, messages }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();

el.streaming = true;
el.clear();
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  el.appendChunk(decoder.decode(value, { stream: true }));
}
el.streaming = false;

5. Send the system prompt

Either fetch the auto-generated system_prompt.txt from the CDN:

const systemPrompt = await fetch(
  "https://asfand-dev.github.io/aktion/dist/system_prompt.txt",
).then((r) => r.text());

…or build a richer prompt programmatically:

const prompt = el.getSystemPrompt({
  mode: "full", // or "chat" for the compact read-only prompt
  preamble: "You are an analytics assistant.",
  additionalRules: ["Always end with a FollowUpBlock of 2 prompts."],
});

Network calls are issued by the LLM-authored code itself via the $http({ url, method, body, ... }) primitive. The host is not involved. Install el.registerHttpInterceptors(...) if you need to attach auth headers, retry on 401, or log every request.

6. (Optional) Listen for assistant messages

Wire LLM-driven follow-ups back into your chat loop:

el.addEventListener("assistant-message", (event) => {
  appendUserMessageToChat(event.detail.message);
});

Public API

All members live on the <aktion-app> element.

Attributes

| Attribute | Values | Description | | --------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------- | | theme | Theme name or JSON token map | Switches the theme. JSON objects are merged on top of the default light tokens. | | streaming | true / unset | Hint that text is still being appended. The error banner is suppressed while set. | | response | Aktion text | Sets the program declaratively. Re-renders whenever the attribute changes. | | src | URL to an .aktion file | Loads the program from an external file resolved relative to the document. The file is linked through the in-browser project linker, so an entry that imports other modules resolves and fetches its whole graph. response (and any inner text) takes precedence; changing src reloads. | | showerrors | true / unset | If present and true, displays parse errors in the rendered UI. Defaults to off. | | strict | true / unset | Dev/strict mode. Surfaces silent failures as console.warns — unknown identifiers that would resolve to null, and trailing {...} objects passed to a user component whose keys match no parameter (the silent named→positional flip). Off by default; enable while developing. | | router-mode | hash (default) / history | URL strategy. history uses the History API for clean /about URLs (needs an index.html fallback on the server); hash works on any static host. | | router-base | path string (e.g. /app) | Sub-directory the SPA is served under, stripped from / prepended to URLs in history mode. | | dir | ltr / rtl / auto | Writing direction. Reflects onto the render root so logical CSS properties, flex order, and text direction flip automatically. Programs need no code change. | | scroll-restoration | auto / top | Opt-in scroll restoration. auto restores per-path scroll on back/forward and jumps to top on fresh navigation; top always jumps to top. |

Routing and JavaScript execution inside effect / action bodies are always available — no host attribute, no allow-list. To omit those surfaces from the generated prompt, build it via getSystemPrompt({ mode: "chat" }).

Properties

| Property | Type | Description | | ------------- | ----------------------------- | -------------------------------------------------------------------------------------- | | response | string | Get or set the current program text. Setter is equivalent to setResponse(text). | | src | string \| null | Reflects the src attribute. Setting it loads (or reloads) the program from that URL. | | streaming | boolean | Reflects the streaming attribute. | | showErrors | boolean | Reflects the showerrors attribute. | | route | string (read-only) | Current path tracked by the router (e.g. "/users/42"). |

Methods

| Method | Description | | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | setResponse(text) | Replace the program (one-shot rendering). Resets state and queries. | | appendChunk(chunk) | Append a streaming chunk and re-render. | | clear() | Reset state, queries, and the rendered output. | | setTheme(name \| tokens) | Apply a built-in theme by name or a partial token map. | | registerComponents(specs, root?) | Extend the built-in library with your own components. | | getSystemPrompt(options?) | Build a system prompt that matches the current library. Pass { mode: "chat" } for the compact variant. | | navigate(path) | Programmatically navigate. Updates window.location.hash. | | registerHttpInterceptors({ onRequest?, onResponse?, onError? }) | Install interceptors for the $http({...}) layer. onResponse receives a retry() one-shot for e.g. 401 refresh flows. | | serializeState() | Return every reactive atom as a plain JSON-friendly object (for SSR / resumption). | | hydrateState(snapshot) | Apply a snapshot to the live store and schedule a re-render. Atoms not in the snapshot are untouched. | | loadSnapshot({ programText, state }) | Atomic program + state load. The next render plans the program with the hydrated state already in place. | | applyDelta(ops) | Apply a structured delta (patch / replace / append / new / delete). User $state is preserved across the diff. |

Module exports

Beyond the element, aktion-runtime exports a set of standalone utilities importable from subpaths:

import { renderToString, renderToStaticMarkup } from "aktion-runtime";
// → { html, state } for SSR; renderToStaticMarkup for SSG

import { htmlToAktion, tailwindToSx, componentSchema, buildGallery, suggestComponent } from "aktion-runtime";
// htmlToAktion(html)           → Aktion program string from common HTML/JSX
// tailwindToSx("p-4 bg-white") → sx object ({ p: "m", bg: "surface", _unmapped: [...] })
// componentSchema()             → stable JSON schema for editor tooling
// buildGallery()                → self-contained HTML component explorer
// suggestComponent("Buttn")     → ["Button", ...] typo suggestions

import { render, within, axe, cleanup } from "aktion-runtime/test";
// render(program, opts) → Screen with Testing-Library-style queries + interactions
// within(node)          → scoped query set
// axe(node)             → a11y audit (returns array of violations)

import { getDiagnostics, getCompletions, formatProgram } from "aktion-runtime/language";
// DOM-free language service for editor integrations

Events

| Event | Detail | When it fires | | -------------------- | --------------------------------------------- | ------------------------------------------------------------------------------ | | assistant-message | { message: string } | When an action or lambda calls $emit("assistant-message", { message: "..." }). | | error | { errors: ParseError[] } | After each render whose source had parse errors. | | route-change | { path, previousPath, source } | When the current hash path changes. source is "init" \| "hashchange" \| "navigate" \| "external". | | <custom-name> | User-defined { ... } | When script calls $emit("name", { ... }) inside an action / effect body. |

The error event always fires regardless of showerrors, so host apps can log or report errors even when the in-page banner is suppressed.

Runtime safety limits

Every program is evaluated under a per-render runtime budget that bounds three independent dimensions so a partial / accidentally recursive program (typed live in the playground, mid-stream LLM token, …) can't freeze the browser:

| Dimension | Default | Triggers on | | ------------------- | ------------ | -------------------------------------------------------- | | componentDepth | 150 levels | function Foo() { return Foo() } and other recursive trees | | iterations | 250 000 / render | unbounded for/while loops inside function bodies | | arrayLength | 100 000 entries | $util.range(0, 1e9), $util.repeat(value, 1e9) |

When a limit trips, the runtime aborts the render, emits an error event whose detail is shaped like a parse error ({ line: 0, column: 0, message }), and leaves the previous tick's DOM intact so the user still sees something useful. The defaults comfortably fit any realistic app; tighten or relax them by constructing a custom budget via createRuntimeBudget({ … }) and passing it through createContext (or pass null to disable enforcement entirely in trusted offline pipelines).


Aktion — the language

Aktion's surface syntax is a strict subset of JavaScript. Every declaration uses standard JS constructs — function, for...of, if/else, switch/case, arrow functions, object literals — so any developer reading the output immediately knows what it does. The renderer commits each line as soon as it streams in, so the user sees the page shell before the leaves arrive.

$count = 0
$theme = "dark"

function Counter(label = "Count") {
  return Stack([
    SectionHeader(label),
    Button("Inc", { onClick: () => $count = $count + 1 }),
    Text(`Current: ${$count}`)
  ])
}

function loadOrders() {
  $orders = $http({ url: "https://api.example.com/orders", method: "GET" })
}

$effect(() => {
  $save = $http({ url: "https://api.example.com/draft", method: "PUT", body: $draft })
}, [$draft, "debounce(500)"])

$orders = $http({
  url:    "https://api.example.com/users/42/orders",
  method: "GET",
  query:  { limit: 5 }
})

pages = $router({
  "/":         Counter(),
  "/orders":   Async($orders, { loading: Spinner(), data: OrderTable($orders.data) }),
  default:     NotFound()
})

$app(pages)

Key constructs

  • $app(…) — the reserved entry point. Every program renders from it. It accepts a single root node, an array of nodes (rendered as siblings), or variadic nodes.

  • $name = value — reactive state. One kind. Read or write with the same sigil. Inside action / effect / lambda bodies, assignment operators (= += -= *= /= ??= ++ --) are all allowed. let/const/var are optional and do not affect reactivity — only the $ prefix makes a value reactive.

  • Component-call shapes — pick ONE per call:

    Component(positionalArg, { prop: value, … })  // canonical
    Component(arg1, arg2, arg3)                   // all-positional, signature order
    Component({ prop: value, … })                 // all-named single object

    The canonical form passes the prop tagged (positional) bare and every other prop in a trailing { } object. All-positional calls bind arguments to the signature's props in listed order — mind that order: Button("Save", "primary") puts "primary" in the second slot (onClick), not variant, so prefer the trailing object for non-adjacent props. A single { } argument whose keys are prop names is an all-named call; when the component's positional prop is itself object-typed, a lone object is that prop's payload instead. One object is never split between the two roles.

  • function Name(p = default) { return Expression } — PascalCase name means it's a component. Parameters use standard JS defaults (=). Inside the body, $x = expr is a declaration: the initializer runs once when the instance first mounts, and re-renders preserve whatever value the user (or an action / effect) has written. Always end with an explicit return. Components do not have a props object — every parameter is a real JS parameter. A custom component may NOT reuse a built-in component's name (the validator flags it) — unless its body calls that same name, the supported wrapper pattern: inside its own body the name resolves to the BUILT-IN, so function Badge(l) { return Badge(l, { tone: "success" }) } extends the library Badge instead of recursing.

  • function name(args) { body } — camelCase name means it's an action. Callable effects with optional return. Used as event handlers (onClick: save) or as expressions ($result = greet("Ada")). Wrap optimistic writes in $optimistic(() => { … }) — it snapshots reactive state, runs the callback, and automatically rolls back if the callback throws (or the promise it returns rejects). An ordinary $-prefixed builtin call, so it works anywhere an expression does.

  • $effect(() => { body }, [...deps]) — declarative, anonymous side effects. The dependency array mixes state triggers ($atom), lifecycle / interval triggers ("mount", "unmount", "every(N)"), and rate-limit modifiers ("debounce(N)", "throttle(N)"). $effect(() => { … }) (no second argument) is equivalent to $effect(() => { … }, ["mount"]). Declare at the program top level for global work, or inside a component function body to scope the effect to that instance — the runtime mounts it on first render and tears down its timers / subscriptions / cleanup(fn) handlers when the instance leaves the tree.

  • $emit("name", { detail }) — dispatch an outbound CustomEvent on the host element. Call from any action / effect / lambda body whenever the surrounding host page needs to react to a user interaction.

  • Standard JS control flow — if, switch, for, while, do…while, try are statements, exactly as in JavaScript. They run inside any imperative body (function, lambda with { … }, effect) but cannot appear on the right-hand side of an assignment. Use ternaries and .map/.filter for value-producing expressions. Bodies may be either a block or a single statement (if (!ok) return):

    banner = $error ? ErrorAlert($error) : Notice("All good")
    rows   = $items.map(item => Row(item))
    // Multi-way dispatch: wrap a `switch` in a function and `return` per arm.
    function viewFor(tab) {
      switch (tab) {
        case "list":  return ListView($items)
        case "grid":  return GridView($items)
        default:      return EmptyState("Pick a view")
      }
    }
    view = viewFor($tab)
  • Full operator set — arithmetic (+, -, *, /, %, **), comparison (== / != / === / !==, < / > / <= / >=), logical (&&, ||, ??), bitwise / shift (&, |, ^, ~, <<, >>, >>>), and the relational keywords instanceof / in. Every compound-assignment form is supported too (+=, -=, *=, /=, %=, **=, &&=, ||=, ??=, &=, |=, ^=, <<=, >>=, >>>=).

  • Line continuations — any expression operator (., ?., ?, :, &&, ||, ??, == / != / === / !==, < / > / <= / >=, instanceof, +, -, *, /, %, **) may appear at the start of the next line and the parser keeps building the same expression — matches JavaScript's ASI rules. Use this to split long method chains, ternaries, and logical expressions across lines.

  • $http({ url, method, headers, body, query, ... }) — the only network primitive (absolute url; GET default; no host-wide defaults). Returns a reactive resource with .data, .error, .status, .loading, .headers, .lastUpdated, .refetch(), .cancel(), and a settable .onDone callback that fires each time the request settles (handy for $todos.refetch() after a write).

  • $query({ url, key?, ttl?, refetchInterval?, refetchOnFocus?, refetchOnReconnect? }) — a cached, deduplicated read built on $http. Identical queries share one bag. Optional ttl auto-refetches stale data; refetchInterval (ms) polls a live feed; refetchOnFocus / refetchOnReconnect refresh on tab focus / reconnect. Add infinite: { param, limit, mode, select } for a paginated list — $feed.loadMore() appends the next page while $feed.hasMore is true. Pass gql (+ optional variables) to POST a GraphQL document and unwrap .data automatically.

  • $mutation({ url, method?, optimistic?, invalidates? }) — a deferred write that fires only when you call .mutate(overrides?) (not on render; method defaults to POST). optimistic: (overrides) => { … } runs synchronously before the request and auto-rolls-back if it fails; invalidates: ["key"] refetches matching cached queries after success. The bag exposes .loading / .error / .data, plus .reset() and a settable .onDone. Use $util.invalidate(keys) to manually trigger cache invalidation from anywhere.

  • $socket({ url, protocols?, bufferSize?, reconnect? }) — reactive WebSocket. Read .status ("connecting" | "open" | "closed"), .connected, .last, .messages, .attempts; call .send(data) (queues while connecting, flushes on open) or .close() (stops for good). reconnect: true (or a max-attempt number) retries dropped connections with exponential backoff. Auto-tears-down on re-plan.

  • $sse({ url, event?, withCredentials?, bufferSize? }) — reactive Server-Sent Events stream with the same .status/.connected/.last/.messages/.close() surface (EventSource reconnects natively).

  • pages = $router({ "/path": Component(), default: NotFound() }) — function-call router. The reserved route handle exposes the reactive surface (route.path, route.params, route.query, route.pattern) and a route.navigate("/path") method. Supports nested layout routes ("/app": { layout: AppShell, routes: {...} } — the shell stays mounted while only the outlet swaps), navigation guards ($util.onNavigate(({ to, from }) => …) — return false to block or a path to redirect), query-param state ($util.url.setQuery("tab","v")), lazy routes (Lazy(() => import(…))), and scroll restoration (set scroll-restoration="auto" on <aktion-app>).

  • Two-way binding is implicit: pass a $variable (or a member chain rooted at one — value: $form.email) as an input prop and the runtime wires it both ways.

  • Lambdas — every JavaScript arrow form is supported: () => expr, x => expr (single param, no parens), (x) => expr, (x, y) => expr, (x = 0) => x + 1 (defaults), (...args) => sum(args) (rest), ({ a, b }) => a + b / ([x, y]) => x (destructured params), (args) => { … } (multi-statement, may return). The body can wrap onto the next line (x =>\n expr).

  • Destructured parameters — both function declarations and lambdas accept array / object patterns (function Card({ title, tone = "info" }), function head([first, ...rest])), with the same defaults / renames / rest support as let-destructuring.

  • JS expression niceties — array / object spread ([...xs, y], { ...base, k: v }, fn(...args)), array / object destructuring in let / const / var (let [a, b, ...rest] = arr, let { name, age = 0 } = user) including nested patterns (let { data: { items: [first] } } = resp), destructuring in for-of heads (for (const [k, v] of Object.entries(obj)), for (const { id } of rows)), computed property keys ({ [$dynamic]: value }), prefix and postfix ++ / -- (with JS-accurate return semantics), new Constructor(...) with trailing member / call chains (new Date(0).getTime()), trailing commas in function params / call args / literal lists. async function is accepted as a no-op modifier; await is allowed in both statement and expression position inside action / effect bodies.

  • Equality & comparison match JavaScript — == / != use abstract-equality coercion (so x == null matches null and undefined, 1 == "1", 0 == false), while === / !== stay strict. Relational < / > compare alphabetic strings lexicographically (alphabetical .sort comparators work) and coerce Date operands via valueOf; two numeric strings still compare numerically ("5" < "10").

  • Top-level imperative statements — if / for / while / try and bare expression statements written at the program top level run once per plan (e.g. building a $state array with a while loop). Inside a render they behave like a module init block; prefer pure expressions (.map, $util.range) where you can.

  • $util runtime namespace — pure, side-effect-free helpers for data shaping, formatting, dates, math, and strings ($util.filter, $util.sort, $util.groupBy, $util.format, $util.formatDate, $util.plural, $util.range, $util.addDays, $util.pick, $util.omit, $util.merge, $util.cloneDeep, $util.chunk, $util.partition, $util.keyBy, $util.zip, $util.flatten, $util.count, $util.slugify, $util.truncate, $util.initials, $util.currency, $util.bytes, $util.relativeTime, $util.uuid, $util.copy (async — resolves true only when the clipboard write succeeds), $util.sleep(ms), $util.debounceFn, $util.throttleFn (leading + trailing edge), …). Never carry hidden state — safe to call anywhere. Also exposes reactive env getters ($util.scroll, $util.viewport, $util.breakpoint, $util.media, $util.mouse, $util.url — lazy listeners, re-render on change; $util.url.setQuery/.removeQuery write query params), styling/validation sub-namespaces ($util.style.cx, .gradient, .alpha, .clamp, .token, .toStyle; $util.rules.required(), .email(), .min(), .pattern(), .custom(), .asyncCustom() — awaited by $form —, .validate(), .validateAll()), computed helper ($util.derived(fn)), side-effect hooks ($util.onError, $util.onNavigate, $util.onRequest, $util.onResponse, $util.invalidate), and device/platform helpers ($util.vibrate, .share, .readClipboard, .geolocate, .isOnline, .deviceType, .worker(pureFn), .registerServiceWorker, .webManifest, .nativeShell, .isNativeApp).

  • Escape hatchesHTMLTag(tag, { attributes?, children? }) for raw HTML elements and Styles(css) for raw CSS injected into the shadow root. Use only when the standard component library cannot express the design.

  • Hoisting & streaming — references resolve from the entire top-level scope, not source order. Always call $app(…) first so the reconciler has the page shell to attach streamed leaves to.

  • Comments: // line comments and /* block */ comments — standard JS style.

Built-in globals at a glance

| Global | Purpose | | --------- | -------------------------------------------------------------------- | | $storage | Browser persistence — $storage.set/get, $storage.session.*, $storage.cookies.*. | | $console | Forwards to the host console — log / error / warn / info / debug. | | $toast | Imperative notifications — $toast.show/.success/.error/.info/.warning, .dismiss(id), .clear(), reactive .items. | | route | Reactive router handle — path, params, query, pattern, navigate(path). | | JS stdlib | The JS standard library — Math, JSON, Object, Array, Number, String, Boolean, Date, Map, Set, RegExp, Promise, plus parseInt / parseFloat / isNaN / isFinite / encodeURIComponent / … Use directly (Math.max(a, b), JSON.stringify(x), Object.keys(o)) or with new (new Date(), new Map()). | | timers | setTimeout / setInterval / clearTimeout / clearInterval — like their JS counterparts, but tracked by the runtime and cleared automatically on re-plan/disconnect. Use inside an effect and clear in cleanup. | | full JS globals | The entire JavaScript global surface is available — dialogs (alert, confirm, prompt), Web APIs (fetch, URL, URLSearchParams, Blob, FormData, crypto, navigator, localStorage, atob/btoa, Intl, BigInt, Reflect, …), and window / document themselves. Any globalThis member resolves by name. |

Both $storage and $console are lowercase; the route handle is reserved (never declare a state slot named route). Author declarations and built-in components always win over a same-named global (a library Text / Map component is never shadowed by the DOM Text / Map), so the global passthrough only resolves names you haven't otherwise defined. For reactive data prefer $http({...}) over raw fetch, and timers/listeners belong inside an $effect(...) so they're cleaned up on unmount.

The 60-second pitch

$days = "7"
$data = $http({ url: "https://api.example.com/metrics", method: "GET", query: { days: $days } })

filter = FormControl("Range", { control: Select("days", {
  items: [SelectItem("7", "7d"), SelectItem("30", "30d")],
  value: $days
}) })
kpi    = StatCard("Events", { value: `${$data.data?.events ?? 0}`, trend: "up" })
chart  = LineChart({
  labels: $data.data?.daily?.day ?? [],
  series: [Series("Events", $data.data?.daily?.events ?? [])]
})

$app(Column([CardHeader("Analytics"), filter, kpi, chart]))

Highlights:

  • One statement per line.
  • Three string flavours: "double", 'single', and `backtick` with ${expression} interpolation.
  • Optional chaining (obj?.prop) and nullish coalescing (a ?? b).
  • Spread in arrays ([...$pinned, ...$todos]) and objects ({...$current, status: "done"}).
  • Array shortcuts: $rows.length, $rows.first, $rows.last, plus pluck ($rows.title[title1, title2, …]).
  • Responsive prop maps on layout components: Grid(items, { columns: { sm: 1, md: 2, lg: 4 }, gap: "lg" }).
  • Forward references are allowed — call $app(Column([...])) first and let the children stream in beneath it.

Declarative todo app

$todos = [{ id: 1, text: "Welcome — try editing", done: false }]
$draft = ""

function add() {
  $todos = [...$todos, { id: $todos.length + 1, text: $draft, done: false }]
  $draft = ""
}

function remove(id) {
  $todos = $util.filter($todos, "id", "!=", id)
}

row = t => Card([Stack([
  Text(t.text),
  Button("Delete", { onClick: () => remove(t.id), variant: "ghost" })
])])

list  = $todos.map(t => row(t))
$app(Stack([
  Input("draft-input", { placeholder: "What needs doing?", value: $draft }),
  Button("Add", { onClick: add, variant: "primary" }),
  list
]))

Fine-grained reactivity

Dependencies are tracked at the path you read, not the whole atom. Reading $user.name subscribes to user.name — so a write to a sibling field leaves that reader untouched, while replacing the whole atom (an ancestor) or writing a descendant still wakes it. This is the auto-tracking of MobX, the path-granularity of Solid, and the "recompute-only-on-input-change" of Redux selectors — with no selectors or special syntax. You just read the path.

$user = { name: "Ada", role: "Engineer" }

// Reads `user.name` → depends on `user.name` only.
greeting = Text(`Hi ${$user.name}`)

// Writing the sibling `role` does NOT re-render `greeting`, recompute a
// `$user.name`-derived value, or fire a `[$user.name]` effect.
function promote() { $user.role = "Manager" }   // greeting stays put
function rename(next) { $user.name = next }      // greeting updates

The rule that keeps it predictable:

| You read… | You depend on… | | --- | --- | | $user | user (the whole atom) | | $user.name | user.name | | $user.address.city | user.address.city | | $rows[i] / $rows.name (array index / pluck) | rows (the array) | | $obj[$key] (dynamic key) | obj + whatever $key reads |

A change to path C wakes a dependency on path D exactly when one is a prefix of the other (equal, ancestor, or descendant) — sibling paths never interfere. Object fields are tracked field-by-field; reading into an array (or through a dynamic key) subscribes at the array/container, so mutating any element re-renders the list. The same model powers the four places work is triggered: render scheduling (the app re-renders only when a changed path overlaps what it displayed), computed values ($total = $util.sum($cart.lines) recomputes only when cart.lines changes), effects ($effect(..., [$user.name])), and per-component re-rendering (below).

Per-component re-rendering

A component only re-executes when its own inputs change — its args (props) or a $state path its body read. This is the granularity of React.memo / Solid, but automatic: no memo() wrapper. If $user.age changes, a ShowName($user.name) that only read name is skipped (its body — and any console.log in it — doesn't run); only the components that actually depend on age re-execute.

function App() {
  $user = { name: "Ada", age: 30 }
  return [ShowName($user.name), ShowAge($user.age)]   // siblings, independent
}
// Changing $user.age re-runs ShowAge only; ShowName is reused (memoized).

Args are compared shallowly (Object.is), so — exactly as in React — passing a fresh inline lambda each render (onClick: () => …) makes the receiving component re-render every time; hoist the handler to a stable binding if you want it skipped. State changes the path-tracker can't see (hook setters, timers, HTTP, effects) fall back to a full re-render.

Granularity is at both the subscription and component level. When a component does re-execute, Aktion rebuilds its render tree and the morph reconciler patches only the DOM that actually differs.

[!IMPORTANT] Path-tracking applies to $name = value atoms only. The other state sources — the $state / $memo / $ref / $reducer hook setters, $http / $query / $mutation lifecycle changes, setTimeout / setInterval ticks, $effect writes, and $emit — cannot be path-tracked, so each of them triggers a full re-render (the morph reconciler still patches only the changed DOM, but every component body re-executes). This is the single most important performance characteristic to internalise: a hook-heavy component tree loses the fine-grained skipping you get from plain atoms. Prefer top-level $name = value atoms for app state on the hot path, and reach for hooks when you specifically need per-instance isolation, accepting the full-re-render cost. See the "Reactivity" section of coding-gen-skill.md for the full model.

Per-instance state & content-addressed identity

function Counter(label) {
  $n = 0
  return Stack([
    Text(`${label}: ${$n}`),
    Button("inc", { onClick: () => $n = $n + 1 })
  ])
}

// Two independent counters — each holds its own atom.
$app(Stack([Counter("A"), Counter("B")]))

Every call site accepts a universal key named argument. The renderer uses it as the instance suffix instead of source location, so reordering siblings keeps per-instance state attached to the right element:

function TaskRow(task) {
  return Stack([Text(task.title)], { key: task.id })
}

Hooks — $state, $memo, $ref, $reducer, $id, and custom $name

A function whose name starts with $ is a hook, mirroring React's use* convention. Hooks are the composable way to manage per-instance state. The built-in hooks mirror their React counterparts one-to-one:

| Hook | React equivalent | Returns | | --- | --- | --- | | $state(initial) | useState | [value, setValue] | | $memo(() => v, [deps]) | useMemo | cached value | | $ref(initial) | useRef | stable { current } box (writes don't re-render) | | $reducer((state, action) => next, initial) | useReducer | [state, dispatch] | | $id(prefix?) | useId | stable unique id per instance |

$state and $memo are the everyday pair:

function Counter() {
  const [count, setCount] = $state(0)
  const label = $memo(() => `Count: ${count}`, [count])
  return Stack([
    Text(label),
    Button("+1", { onClick: () => setCount(c => c + 1) })
  ])
}

$app(Counter())
  • $state(initial) returns a [value, setValue] pair. setValue(next) replaces the value; setValue(prev => next) derives it from the previous value. The initializer is evaluated once, on first render.
  • $memo(() => compute, [deps]) returns a cached value and recomputes only when a dependency changes (shallow Object.is compare). Omit the deps array to recompute every render.
  • $ref(initial) returns a stable mutable { current } box whose identity persists across renders. Writing ref.current = … does not schedule a re-render — the escape hatch for holding a DOM node, a timer id, or a previous value. Pair it with OnMount(child, { onMount: node => ref.current = node }) to grab a rendered DOM node.
  • $reducer((state, action) => next, initial) returns [state, dispatch]. dispatch(action) runs the reducer and re-renders when the result changes — the clean way to manage many related state transitions.
  • $id(prefix?) returns a stable, unique string id for the instance's lifetime — for wiring for / id / aria-labelledby pairs without hard-coding ids that collide across multiple instances.

Declare your own hooks with function $name(...). A custom hook's body runs inline in the calling component's hook scope, so its $state / $memo calls attach to that component — exactly how a React custom hook shares its caller's slots:

function $useCounter(start) {
  const [count, setCount] = $state(start)
  return { count: count, increment: () => setCount(c => c + 1) }
}

function Counter(label) {
  const c = $useCounter(0)
  return Stack([Text(`${label}: ${c.count}`), Button("+1", { onClick: c.increment })])
}

Two rules, both inherited from React: call hooks unconditionally and in a stable order at the top level of a component / hook body (slots are matched by call order across renders), and remember that hook state resets when the instance leaves the tree — a remounted component starts again from its initial value. $state, $memo, $ref, $reducer, and $id are reserved names. The lighter $name = value per-instance form above remains available when an atom is written directly by the component's actions.

Global stores — $store({...})

For state shared across components — the role Redux / Zustand / Pinia play elsewhere — declare a store. Non-function entries are reactive state; function entries are methods that receive the store handle s first. Read state with store.field (fine-grained), call methods with store.method(args), and mutate inside a method with s.field = …. The handle is an app-global singleton with reference-stable methods, so any component reads it or calls its actions directly — no prop drilling.

cart = $store({
  items: [],                                          // state
  count: (s) => s.items.length,                       // getter → cart.count()
  total: (s) => $util.sum(s.items.map(i => i.price)),  // getter → cart.total()
  add: (s, item) => { s.items = [...s.items, item] }, // action → cart.add(item)
  clear: (s) => { s.items = [] },
})

// Siblings with no relationship both talk to the same cart.
function AddLatte() { return Button("Add", { onClick: () => cart.add({ price: 4.5 }) }) }
function MiniCart() { return Text(`${cart.count()} items — ${$util.format(cart.total(), "currency")}`) }
$app(Column([AddLatte(), MiniCart()]))

Reads are fine-grained and per-component (changing cart.items re-renders only components that read it), and store fields support two-way binding (Input(value: form.draft)). Use a $store for shared state; use a component's local $state / $name = value for state one component owns. See the Global state guide.

Persistence. Add persist: "key" and the store's data round-trips to localStorage on every change and hydrates on mount. Use persistIn: "session" for sessionStorage. persist and persistIn are config-only — they're never exposed as state fields.

Undo/redo. Add history: true (or a depth cap) and the store records per-mutation snapshots. store.undo() / store.redo() / store.clearHistory() plus reactive store.canUndo / store.canRedo for wiring button disabled states.

doc = $store({
  persist: "my-doc",   // survives reload
  history: 25,         // undo/redo up to 25 steps
  title: "Untitled",
  setTitle: (s, v) => { s.title = v }
})
// doc.undo() / doc.redo() / doc.canUndo / doc.canRedo

Component-scoped effects

$effect(() => { … }, [...deps]) blocks can live at the program top level or inside a component function body. Inside a component body the runtime mounts the effect when the instance first renders and tears it down (clearing timers, unsubscribing watched atoms, firing every registered cleanup(fn)) the moment the instance disappears from the tree. Two LiveClock() calls produce two independent intervals — and removing one stops only that one:

$app(Stack([LiveClock("UTC"), LiveClock("Local")]))

function LiveClock(label) {
  $now = $util.now()
  $effect(() => {
    $now = $util.now()
  }, ["every(1000)"])
  return Stack([Text(label), Text($util.formatDate($now, "time"))])
}

Use a top-level $effect(() => { … }, [...]) for global work (analytics, app-wide keyboard shortcuts, hydration of shared atoms); use a component-local effect whenever the background work logically belongs to the UI it serves.

Schema-as-truth diagnostics

validateProgramSchema(program, library) (exported from src/library/index.js) emits hard errors for:

  • Closed-token enum mismatches (Button("Save", { variant: "magic" })).
  • Unknown named args (Stack({ junk: 1 })).
  • One-positional-max violations (Button("Save", "primary", true) → "use { variant: "primary", loading: true }").

The host element merges these into program.errors so the on-screen banner surfaces every violation.

Anticipatory skeletons

A reference to a component that hasn't been declared yet (and isn't in the library) renders a Skeleton placeholder instead of [unknown component: …]. Mid-stream forward references just shimmer until the next render pass picks the declaration up.

For the complete language reference see docs/language.html or, for full apps, the deep authoring guide coding-gen-skill.md.


Component library

The bundle ships 275 components grouped by domain. Reach for pattern composites (Hero, PageHeader, Stats, Toolbar, EmptyState, Timeline, KanbanBoard, DescriptionList, PricingTable, …) before hand-rolling the equivalent with Card + Stack — they're tuned to produce dense, production-quality SaaS UI in a single line.

| Group | Components | | ------------------ | ---------- | | Layout | Column, Row, Center, Stack, StackItem, Grid, GridItem, Container, Box, Spacer, Card, CardHeader, CardFooter, Separator, Tabs, TabItem, Accordion, AccordionItem, Modal, Drawer, Steps, AspectRatio, ScrollArea, Sticky (with a data-stuck pinned hook), ResizablePanels, MasonryGrid, Section (page band with eyebrow/title/subtitle), Split (sticky two-pane), Bento/BentoCell (asymmetric grid), Overlay/OverlayItem (anchored layering), Fragment | | Content | Text, Image, Icon, Badge, BadgeList, Callout, Quote, CodeBlock, Skeleton, Spinner, Markdown, Kbd | | Forms | Form, FormControl, FormSection, FieldSet, ValidationSummary, Input, TextArea, PasswordInput, MaskedInput, MentionInput, TagInput, Select, SelectItem, Combobox, MultiSelect, Checkbox, CheckBoxGroup, CheckBoxItem, Radio, Switch, ToggleGroup, Button, Buttons, SearchBar, Slider, NumberInput, ColorPicker, DatePicker, DateRangePicker, TimePicker, DateTimePicker, FileUpload, PinInput, MultiStepForm | | Data | Table, Col, DataGrid, List, ListItem, StatCard, Stats, Sparkline, Tile, Progress, ProgressRing, Pagination, Tree, TreeNode, CalendarView, ComparisonTable, InfiniteList | | Charts | BarChart, LineChart, PieChart, RadarChart, ScatterChart, Histogram, Heatmap, Gauge, Series | | Feedback & Media | Avatar, AvatarGroup, PersonChip, Tooltip, HoverCard, Popover, Rating, Toast, VideoPlayer, AudioPlayer, Carousel, Gallery, Lightbox, Map | | Navigation | Breadcrumb, BreadcrumbItem, Navbar, NavbarItem, TopBar, NavLink (router-aware) | | Menus | DropdownMenu, MenuItem, MenuSeparator, MenuLabel, ContextMenu | | Editors | RichTextEditor, CodeEditor | | Chat | SectionBlock, ListBlock, FollowUpBlock, FollowUpItem, ActionLink, ChatBubble | | Patterns | Hero, PageHeader, SectionHeader, Toolbar, EmptyState, Timeline, TimelineItem, ActivityLog, FeatureGrid, FeatureItem, MediaCard, Testimonial, ProfileCard, Comment, Banner, Notification, InboxPanel, OnboardingChecklist, KanbanBoard, KanbanColumn, KanbanCard, DescriptionList, DescriptionItem, StatusDot, PricingTable, PricingCard, LoadingState, ErrorState, SuccessState, Tour, Spotlight | | App shell | AppShell, Sidebar, SidebarSection, SidebarItem (supports to for router navigation), SplitView | | Advanced UI | IconButton, CommandPalette, FilterChips, FieldRepeater, VirtualList, QueryBuilder, DiffViewer, JsonTree, Gantt, Truncate, InlineEdit, NotificationBell | | Marketing | NavBar (sticky/blur + mobile burger menu), Brand, Footer, FooterColumn, LogoCloud, LogoChip, Display, Heading, Eyebrow, GradientText, CountUp, Metric, MetricStrip, CodeWindow, BrowserFrame, Terminal, Backdrop (grid/blobs/particles), ThemeToggle, Swatch, Prose | | E-commerce | ProductCard, PriceTag, QuantityStepper, VariantSelector, OrderSummary, Cart | | Motion & gestures | Reveal (scroll-triggered), Transition (enter/exit), FlipList (FLIP reorder), RouteView (route transitions), Parallax, OnGesture (swipe/pan/longPress/doubleTap), Sortable, Draggable, DropZone, Confetti, Lottie | | Overlays | Sheet, BottomSheet, ConfirmDialog — all with Escape-to-close, a Tab focus trap, and focus restore | | Content & docs | TableOfContents, ReadingProgress, ScrollSpy, AuthorByline, ShareButtons, RelativeTime, CopyButton, KbdShortcut, QRCode, Svg (sanitised inline SVG) | | Realtime & social | TypingIndicator, PresenceAvatars, ReactionPicker, LiveCursor, TabBar (mobile bottom nav) | | Scheduling | Calendar (month grid with arrow-key navigation, event chips/dots), CountdownTimer | | Canvas | DrawingCanvas, SignaturePad | | Accessibility | VisuallyHidden, SkipLink, LiveRegion, FocusTrap | | Utility | SegmentedControl, FloatingActionButton, SpeedDial, BackToTop, VirtualGrid (windowed 2-D grid) | | Helpers | Async, Show, Portal, Redirect, Lazy, ErrorBoundary | | Behaviour wrappers | OnClick, OnMouse, OnKeyboard, OnFocus, OnIntersect, OnMount, Css, Link — attach click / mouse / keyboard / focus / intersection / lifecycle listeners or raw class / style to ANY component without it needing a dedicated prop. OnMount(child, { onMount, onUnmount }) is the DOM-ref escape hatch — onMount(node) fires once after attach so you can measure, focus, or hand the node to an imperative library. Link(label_or_child, { to?, href?, external? }) wraps either a string or a component as a router-aware anchor. | | Interop | Mount, WebComponent — host an imperative / third-party widget (chart, map, editor, payment element) that owns its own DOM. Mount({ setup, update?, cleanup?, props?, tag?, sx? }) gives a managed setup → update → cleanup lifecycle; WebComponent(tag, { attributes?, properties?, on? }) renders + hydrates any native custom element with reactive attributes / events. Pair with the $script({ src, global? }) loader and the $dom observer namespace — see interop.html. | | Escape hatches | HTMLTag, Styles (last-resort raw HTML / CSS — see language.html) | | Theming | $theme | | Routing | $router({ … }), NavLink |

Form onChange callback

Every input component accepts an optional onChange(value) callable that fires with the freshly-read value on every change. Use it alongside (or instead of) $variable two-way binding when you need to react beyond a state write (debounce a search, persist a setting, kick off a fetch).

Input("query", { onChange: q => $results = $http({ url: `https://api.example.com/search?q=${q}` }) })
Slider("vol", { min: 0, max: 100, value: $vol, onChange: v => $storage.set("volume", v) })
Switch("dark", { value: $theme == "dark", onChange: on => $theme = on ? "dark" : "light" })

Behaviour wrappers

Six tiny wrappers attach behaviour to any component:

// Clickable / tappable card
OnClick(Card([Text("View order")]), { onClick: () => route.navigate("/orders/4821") })

// Lazy-load sentinel — fires once when the placeholder scrolls into view
OnIntersect(Skeleton({ variant: "card" }), { onEnter: $items.refetch, once: true })

// Drop zone — uses standard HTML5 drag-and-drop
OnMouse(Card([Text("Drop files here")]), {
  dragOver: e => e.preventDefault(),
  drop: e => { e.preventDefault(); $files = e.dataTransfer.files }
})

// Apply a custom class / style without breaking out of the component
Css(Card([Text("Highlighted")]), { class: "highlight", style: "border-color: #f59e0b;" })

OnClick / OnMouse / OnKeyboard / OnFocus render the child via a transparent wrapper (display: contents) so the visual tree is unchanged — only events bubble through the wrapper. OnIntersect uses IntersectionObserver and disposes cleanly when the component leaves the tree. Css merges classes / inline styles directly onto the rendered child element. Link is the same wrapper applied as an <a> anchor with optional router-aware navigation (to).

The full catalog with positional signatures, prop tables, enum values, and live previews is at docs/components.html.

Rich pattern composites

function export_q3() { $exp = $http({ url: "https://api.example.com/exports/q3", method: "POST" }) }
function new_project() { route.navigate("/projects/new") }

dashHeader  = PageHeader("Engineering Q3", { subtitle: "12 active · 4 at risk", breadcrumbs: ["Workspace", "Engineering"], actions: dashActions, status: Badge("On track", "success") })
dashActions = [Button("Export", { onClick: export_q3, variant: "secondary" }), Button("New project", { onClick: new_project, variant: "primary" })]
kpis        = Stats([
  StatCard("Active",  { value: "12",  trend: "flat" }),
  StatCard("At risk", { value: "4",   trend: "up",   delta: "+2" }),
  StatCard("Shipped", { value: "8",   trend: "up",   delta: "+3" }),
  StatCard("On-time", { value: "87%", trend: "down", delta: "-3%" })
])
board = KanbanBoard([
  KanbanColumn("To do",  { items: [KanbanCard("Migrat