@llui/dom
Runtime for the LLui web framework — The Elm Architecture on a compile-time-optimized signal runtime.
No virtual DOM. view() runs once at mount, building real DOM nodes with reactive bindings; a chunked-mask reconciler updates only the bindings whose dependency paths actually changed.
Install
pnpm add @llui/dom
Quick Start
import { component, mountApp, button, text } from '@llui/dom'
type State = { count: number }
type Msg = { type: 'inc' } | { type: 'dec' }
const Counter = component<State, Msg>({
name: 'Counter',
init: () => ({ count: 0 }),
update: (state, msg) => {
switch (msg.type) {
case 'inc':
return { ...state, count: state.count + 1 }
case 'dec':
return { ...state, count: state.count - 1 }
}
},
// `state` is a Signal<State> — derive reactive values with `.map` / `.at`.
view: ({ state, send }) => [
button({ onClick: () => send({ type: 'dec' }) }, [text('-')]),
text(state.map((s) => String(s.count))),
button({ onClick: () => send({ type: 'inc' }) }, [text('+')]),
],
})
mountApp(document.getElementById('app')!, Counter)
The view bag and signal handles
view receives { state, send }. state is a Signal<State> — a read handle, not the value:
state.at('field')— narrow to a sub-path signal (state.at('user').at('name')).state.map((s) => …)— derive a reactive value; the binding's mask tracks exactly the paths read.state.peek()— one-shot read, for handlers / effects /onMount(never as a slot value).
Element helpers (div, button, …) and structural primitives (each, show, branch, …) are module imports, not bag members. Combine multiple signals with derived([a, b], (av, bv) => …).
Mountable — everything you build is a lazy description
Every authoring helper (el/div/text/each/show/branch/unsafeHtml/lazy/virtualEach/foreign/portal/provide) returns a Mountable — a recipe materialized into live DOM at the point it is placed (as an element child, or in a view / arm / row return). Consequences:
- Annotate view helpers
Renderable(readonly Mountable[]— a list) orMountable(a single element) — notNode/Node[]. - Capture and reuse freely. A
Mountablestored in a variable and reused across ashow/branchremount rebuilds fresh each time; placing one twice yields two independent live instances. - Side-effect helpers must be placed.
onMount(cb)registers nothing unless its returnedMountableis in the view array — it is not an eager side effect. - Raw DOM interop: wrap an existing node with
mountable(() => node).
API
Core
| Export | Purpose |
|---|---|
component(spec) |
Define a component (init / update / view / onEffect?) |
mountApp(el, def) |
Mount a component into a container element |
mountSignalComponent(target, def, opts?) |
Lower-level mount — container or { anchor } target, optional hydrate |
hydrateSignalApp(target, def, serverState) |
Hydrate server-rendered HTML |
derived(sigs, fn) |
Combine N signals into one derived signal |
pathHandle / isSignalHandle |
Construct / detect a runtime signal handle |
tagSend(handler, variants) |
Tag an event handler with the msg variants it can send (agent protocol) |
View content
| Export | Purpose |
|---|---|
text(value) |
Reactive (or static) text node |
unsafeHtml(value) |
Render a raw HTML string (escape hatch; caller owns sanitization) |
each(items, { key, render }) |
Keyed list |
show(cond, render, orElse?) |
Conditional render (the condition signal is narrowed for the arm) |
branch(value, discriminant, arms) |
Discriminated-union / keyed render |
virtualEach(opts) |
Windowed keyed list (uniform number or per-item (item, index) => number row height) |
lazy(opts) |
Async-loaded child component with fallback / error |
foreign(spec) |
Imperative-library boundary (declared signals → LiveSignals) |
portal(content, target?) |
Render into a different DOM location (default document.body) |
onMount(cb) |
Run after mount; return a cleanup. Place the returned marker. |
mountable(build) |
Wrap a build closure / raw node as placeable content |
| element helpers | div, span, button, input, a, h1–h6, ul/li, table/tr/td, svg/path/…, 60+ total |
Context
| Export | Purpose |
|---|---|
createContext(default, name?) |
Create a context |
provide(ctx, value, render) |
Provide a value to everything render builds |
useContext(ctx) |
Read the nearest provided value (or the default) |
SSR
| Export | Purpose |
|---|---|
renderToString(def, state, env) |
Render a component to an HTML string |
renderNodes(def, state, env) |
Render to detached nodes (adapter use) |
serializeNodes(nodes) |
Serialize nodes to an HTML string |
browserEnv / DomEnv |
SSR document-env contract (pick a backing DOM below) |
Sub-path Exports
import { installSignalDebug } from '@llui/dom/devtools' // dev/agent relay — kept out of prod bundles
import { jsdomEnv } from '@llui/dom/ssr/jsdom' // server: jsdom-backed DomEnv
import { linkedomEnv } from '@llui/dom/ssr/linkedom' // server: linkedom-backed DomEnv
import { subApp } from '@llui/dom/escape-hatch' // isolated child TEA loop (rare)
// '@llui/dom/internal' — render-context glue for sibling adapter packages (e.g. @llui/vike)
Performance
Competitive with the fastest fine-grained reactive frameworks on js-framework-benchmark — see the benchmarks page.
Functions
__currentBuildInfo()
Adapter hook (@llui/vike): the build currently in progress, or null when
called outside a signal build. Exposes the build's doc (to create anchor
nodes that belong to the same document as the surrounding tree) and a SNAPSHOT
of the context values in scope at the call site (so an adapter that mounts a
NESTED build in a separate pass can replay them via runBuild's seedContexts
/ the contexts mount option). Returns a fresh snapshot map — safe to retain.
function __currentBuildInfo(): {
doc: SignalDoc
contexts: ReadonlyMap<symbol, unknown>
} | null
__registerScopeVariants()
Compiler-emitted (signal connect-translator path) + library helper: register the variants for the active build scope. No-op outside a build.
function __registerScopeVariants(variants: readonly string[]): void
applyAttr()
Apply a single non-reactive prop VALUE to node: style.* → individual style
property, form-control IDL props (value/checked/selected/indeterminate)
→ live property assignment, everything else → content attribute (null/false
removes, true sets empty). Exported so a compiler-emitted {@link RowFactory}'s
reactive-prop commit routes through the same DOM-application logic the
authoring path uses (rather than re-inlining the IDL/style quirks).
function applyAttr(node: Element, name: string, value: unknown): void
base()
Set the document <base> (one per document — dedups to a single tag).
function base(attrs: BaseAttrs): Mountable
bodyAttr()
Set attribute(s) on <body> (e.g. bodyAttr({ class: theme })).
function bodyAttr(attrs: Record<string, HeadValue<string | boolean | null>>): Mountable
branch()
Discriminated-union render. discriminant selects the union's tag field
(v => v.kind, v => v.type, …); each arm receives the NARROWED variant
signal, so it can read variant-only fields with full types (v.at('data')).
Mirrors show's narrowing. Rewritten by the compiler to signalBranch.
export function branch<U extends object, D extends keyof U>(
value: Signal<U>,
discriminant: (u: U) => U[D],
arms: {
[K in U[D] & (string | number)]: (v: Signal<Extract<U, Record<D, K>>>) => Renderable
},
/** Optional element-level transition hooks — animate the arm swap (see `show`). */
transition?: TransitionOptions,
): Mountable
export function branch<K extends string | number>(
value: Signal<K>,
arms: Partial<Record<K, () => Renderable>>,
/** Optional element-level transition hooks — animate the arm swap (see `show`). */
transition?: TransitionOptions,
): Mountable
browserEnv()
Wrap the browser globals as a DomEnv. Used as the default env for
mountApp / hydrateSignalApp on the client.
The returned object delegates to globalThis.document / globalThis.X
lazily — evaluating browserEnv() on a server process before a DOM
exists is safe because the delegation only dereferences the globals
when a method is actually called.
Never mutates globalThis. A process with no browser globals that
invokes one of the factory methods gets a TypeError / ReferenceError
at the call site — which is correct: you're trying to build DOM on a
runtime that has no DOM.
function browserEnv(): DomEnv
collectHeadSink()
function collectHeadSink(): CollectHeadSink
component()
Define a signal component. Identity at runtime — the view has been lowered by
the compiler; the authoring/runtime bag shapes coincide (state: Signal).
The three type parameters:
S— State. The component's state shape. Must be JSON-serializable (plain objects/arrays/primitives — no class instances, functions, Maps, or Dates) so it can be snapshotted, time-travelled, and sent over the agent wire. Inview,statearrives as aSignal<S>— read it withstate.at('field'),state.map(fn), orstate.peek()(handlers/effects).M— Msg. The message/action union the reducer handles. A discriminated union with atypefield ({ type: 'inc' } | { type: 'set'; value: number }); thetypediscriminant is what the compiler, devtools, and agent surface key off. Enforced byM extends { type: string }.E— Effect. The effect union returned frominit/update, also a discriminated union with atypefield. Defaults tonever(a pure component with no effects). Handled inonEffect(or by@llui/effects). Spelling these out (and the{ type: string }constraint) catches a malformed Msg/Effect union at the call site instead of at the first failed dispatch.
function component<S, M extends { type: string }, E extends { type: string } = never>(spec: SignalComponentSpec<S, M, E>): SignalComponentDef<S, M, E>
createContext()
function createContext<T>(defaultValue: T, name = 'context'): Context<T>
currentDoc()
The document of the ACTIVE build — a live client Document on the client, a
server DomEnv under SSR. Public so sibling modules can create nodes / read
head/documentElement/body in the same environment the runtime builds in.
Must be called during a build.
function currentDoc(): SignalDoc
derived()
Combine N independent signals into one derived signal. Use when the inputs have
no shared parent signal (cross-tree, or a per-row item signal + a component-state
signal); for a single source, prefer {@link Signal.map}.
Two call forms — pick whichever reads cleaner:
derived(a, b, (va, vb) => …) // variadic: 2–4 sources, positional values
derived([a, b, …c], (…vals) => …) // array: any N, tuple-typed values
The compiler lowers derived(...) inside a DIRECT view to an inline call. This
is the equivalent RUNTIME handle for view-helper composition (where there is no
statically-known path): produce/peek apply fn over the resolved sources and
deps is the UNION of the sources' deps — so the chunked-mask reconciler fires
the binding whenever ANY source changes, and commits only on an output change.
All inputs must resolve against the same binding state (the common case: each is
rooted at the component state, or all at the same row ctx). The result is a
{@link MappedSignal} — like a .map(), it carries no path, so .at() on it is
a compile error (slice the sources before combining).
export function derived<A, B, U>(a: Signal<A>, b: Signal<B>, fn: (a: A, b: B) => U): MappedSignal<U>
export function derived<A, B, C, U>(
a: Signal<A>,
b: Signal<B>,
c: Signal<C>,
fn: (a: A, b: B, c: C) => U,
): MappedSignal<U>
export function derived<A, B, C, D, U>(
a: Signal<A>,
b: Signal<B>,
c: Signal<C>,
d: Signal<D>,
fn: (a: A, b: B, c: C, d: D) => U,
): MappedSignal<U>
export function derived<T extends readonly unknown[], U>(
sigs: { readonly [K in keyof T]: Signal<T[K]> },
fn: (...values: T) => U,
): MappedSignal<U>
domHeadSink()
Build a sink that writes to doc's live <head> / <html> / <body>. Returns
an inert sink when the document has no <head> (server DomEnv — those go
through the collector instead).
function domHeadSink(doc: SignalDoc): HeadSink
each()
function each<T>(items: Signal<readonly T[]>, opts: {
key: (item: T) => string | number
render: (item: Signal<T>, index: Signal<number>) => Renderable
/** Optional element-level transition hooks (from `@llui/transitions` — e.g.
* `fade()`, `flip()`, `mergeTransitions(fade(), flip())`): `enter` animates
* newly-inserted rows, `leave` defers a removed row's detach until its promise
* resolves, and `onTransition` runs after each keyed reconcile so FLIP can
* glide surviving rows to their new positions. Omitted ⇒ no animation (the
* reconcile is unchanged). Never runs under SSR. */
transition?: TransitionOptions
}): Mountable
eachArm()
Compiled render-arm keyed list — the MID-TIER between {@link eachDirect}
(full direct construction) and the verbatim authoring {@link each}: the items
source is a verbatim runtime handle (a view-helper's call-site-bound signal the
compiler can't resolve), but the ROW is a compiled () => [...] arm whose
binding producers read the combined row ctx (ctx.item / ctx.index) directly
— no per-row item/index handle allocation. Un-lowerable children inside the arm
(a nested show on a state handle, a helper call without the row param) stay
verbatim and run via the authoring path within the row build. Emitted by the
compiler's pass-2 helper-each lowering when the row factory bails on a
structural child.
function eachArm<T>(items: Signal<readonly T[]>, key: (item: T) => string | number, render: (getCtx: () => RowCtx<T>) => Renderable, stateDeps?: readonly string[]): Mountable
eachDirect()
Direct-construction keyed list. Same keyed reconcile as {@link each}, but each
row is built by row (a {@link RowFactory}: direct DOM + binding specs wired by
node reference) instead of authoring helpers — the compiled fast path. The
factory's spec produce(ctx) reads the row ctx { item, state, index }.
stateDeps names the component-state paths the factory's bindings read (the
compiler passes the collected set, often empty); omitted (legacy emissions),
it falls back to whole-state so ctx.state reads stay live.
function eachDirect<T>(items: Signal<readonly T[]>, key: (item: T) => string | number, row: RowFactory, stateDeps?: readonly string[]): Mountable
el()
Build an element. on* function props become event listeners; react(...)
props become reactive bindings; everything else is a static attribute. Returns a
Mountable that creates the element and materializes its children when placed.
function el(tag: string, props: Readonly<Record<string, PropValue>> = {}, children: readonly ChildNode[] = []): Mountable
elNS()
Build an SVG-namespaced element (svg/path/g/circle/…). Same prop/child
semantics as el, via createElementNS.
function elNS(tag: string, props: Readonly<Record<string, PropValue>> = {}, children: readonly ChildNode[] = []): Mountable
foreign()
Embed an imperative library. Declared state signals are materialized to
LiveSignals for mount. A REAL runtime helper (like text/each/show/branch):
the compiler lowers a direct-view foreign() to signalForeign, but in
view-helper functions / uncompiled code it runs here — converting each declared
state HANDLE to its {produce, deps} spec and delegating to signalForeign.
function foreign<Inst, State extends Record<string, Signal<unknown>>>(spec: {
tag?: string
state?: State
mount: (args: {
el: Element
state: { [K in keyof State]: LiveSignal<State[K] extends Signal<infer T> ? T : unknown> }
}) => Inst
unmount?: (instance: Inst) => void
}): Mountable
htmlAttr()
Set attribute(s) on <html> (e.g. htmlAttr({ lang })). Each attribute
dedups independently and restores its pre-existing value on unmount.
function htmlAttr(attrs: Record<string, HeadValue<string | boolean | null>>): Mountable
hydrateSignalApp()
Hydrate a signal component over server-rendered HTML in container. Builds the
client tree against serverState (matching the SSR render) and atomically
swaps it in — server HTML stays visible until the swap, so no flash. init()'s
effects are skipped by default (already run on the server); pass
runInitEffects: true for init()s that no-op on the server.
function hydrateSignalApp<S, M, E = never>(target: Element | MountTarget, def: SignalComponentDef<S, M, E>, serverState: S, options?: { runInitEffects?: boolean; contexts?: ReadonlyMap<symbol, unknown> }): SignalComponentHandle<S, M>
installSignalDebug()
Build the signal debug API and register it. Returns an unregister function.
function installSignalDebug(hooks: SignalDebugHooks): () => void
isMountable()
function isMountable(v: unknown): v is Mountable
isSignalHandle()
function isSignalHandle(v: unknown): v is SignalHandle<unknown>
lazy()
Load a signal component asynchronously: render fallback() immediately, then
swap in the loaded component when loader() resolves (or error(err) on
reject). Identity at runtime — a real runtime helper (not compiled away), so
view-helper composition and uncompiled tests can call it directly.
function lazy<LS = unknown, LM = unknown, LE = unknown>(opts: SignalLazyOptions<LS, LM, LE>): Mountable
link()
Add a <link> tag (canonical, preload, stylesheet, …). Dedups by rel+href.
function link(attrs: LinkAttrs): Mountable
mergeStaticHead()
Merge a static +Head.ts head string with collected component head, letting
component entries WIN: any <title> / <meta name|property> in staticHead
whose key the component also set is stripped, so the document never carries two
<title>s (the browser would silently use the first). Returns
strippedStatic + collected.head.
function mergeStaticHead(staticHead: string, collected: CollectedHead): string
meta()
Add a <meta> tag. Dedups by name/property/httpEquiv/charset.
function meta(attrs: MetaAttrs): Mountable
mountable()
Wrap a build closure as a Mountable. build runs (with a live ctx) when the
Mountable is placed — see populate/runBuild. Public so adapter packages
(@llui/vike's pageSlot) and raw-DOM interop can produce placeable view content:
mountable(() => someRawNode). Note the build runs once per placement, so a build
that returns a captured node (rather than creating a fresh one) reintroduces the
single-parent footgun — create the node inside the closure.
function mountable(build: () => Node): Mountable
mountApp()
Mount a signal component into a container.
function mountApp<S, M, E = never>(container: Element, def: SignalComponentDef<S, M, E>, opts?: MountSignalOptions<S>): SignalComponentHandle<S, M>
mountSignal()
Mount a signal view: build the nodes (collecting bindings), attach them at the
target, and wire a chunked-mask reconciler over the collected bindings.
For a container target, 'append' (fresh mount) leaves existing children and
'replace' swaps server HTML out atomically (hydration). For an anchor target,
the nodes are inserted immediately after the anchor comment and bracketed by a
synthesized end sentinel — dispose() removes that bracketed region.
seedContexts seeds the build's root context values (see runBuild); used by
adapters mounting a nested build whose providers live in a different pass.
function mountSignal(target: Element | MountTarget, initial: unknown, build: () => Renderable, modeOrSeed?: 'append' | 'replace' | ReadonlyMap<symbol, unknown>, seedContexts?: ReadonlyMap<symbol, unknown>, getState?: () => unknown): SignalMount
mountSignalComponent()
Mount a signal component and drive its update loop. The target is a container
Element (fresh mount appends; hydration replaces) OR a MountTarget
descriptor — including { anchor } for adapters mounting a nested layer as
siblings of a slot anchor. With opts.hydrate, takes over server-rendered
HTML (see MountSignalOptions).
function mountSignalComponent<S, M, E = never>(target: Element | MountTarget, def: SignalComponentDef<S, M, E>, opts?: MountSignalOptions<S>): SignalComponentHandle<S, M>
normalizeUpdateResult()
Normalize an init() / update() result — a [state, effects] tuple or a
bare state (the convenience return) — to a [state, effects] pair. This is
the ONE place the [S, E[]] | S heuristic lives; component, SSR, and (later)
@llui/test / @llui/vike all route through it so the shape decision never
diverges.
The heuristic: a 2-element array whose SECOND element is itself an array is
read as [state, effects]; anything else is a bare state with no effects.
KNOWN AMBIGUITY (deliberately unchanged — dropping the bare-S convenience is
a repo-wide breaking ripple, out of scope here): a state that is ITSELF a
2-tuple whose second element is an array (e.g. the whole state is
[number, string[]]) is mis-read as [state, effects]. Such a state must be
returned explicitly as [state, []].
function normalizeUpdateResult<S, E>(r: [S, E[]] | S): [S, E[]]
noscript()
Add a <noscript> with body as its text content.
function noscript(body: HeadValue<string>): Mountable
onMount()
Register a callback to run after the surrounding view's nodes are mounted, receiving the mounted parent element. Returning a function registers a teardown (run on unmount / dispose). Returns a marker node for the view array.
function onMount(cb: (root: Element) => void | (() => void)): Mountable
onTeardown()
Register a teardown to run when the owning scope is disposed (unmount). Public companion to {@link registerBinding} for sibling modules. Must be called during a build.
function onTeardown(fn: () => void): void
pathHandle()
A path-rooted handle: produce resolves base from the binding state;
peek reads the live value via get. at extends the path; map derives.
rowLocal marks a handle rooted at a ROW ctx (the internal rowHandle for
item/index); it propagates through .at/.map so row locality is carried,
never string-inferred. Component-state handles default to false.
function pathHandle<T>(get: () => unknown, base: string, rowLocal = false): SignalHandle<T>
portal()
Render content into target (default document.body) instead of inline —
for overlays (dialog/popover/toast). The content's bindings join the current
scope (so it stays reactive); a teardown removes the nodes on unmount/dispose.
Returns an inline placeholder comment.
function portal(content: () => Renderable, target?: Element): Mountable
provide()
Provide value for context to everything render builds, then restore.
function provide<T>(context: Context<T>, value: T, render: () => Renderable): Mountable
react()
function react(produce: Producer, deps: readonly string[], componentRooted?: boolean): Reactive
registerBinding()
Register a reactive binding into the ACTIVE build whose commit applies the
value to a custom target (not an inline element). Public so sibling modules
(e.g. head/metadata management) can build bindings with non-element commit
targets that still ride the component's one chunked-mask reconciler — produce
runs on mount and whenever deps chunks go dirty; commit gets the new value.
Must be called during a build (inside a mountable(...) recipe).
function registerBinding(deps: readonly string[], produce: (state: unknown) => unknown, commit: (value: unknown) => void): void
renderNodes()
Build a signal component's DOM tree on the server, returning the (detached)
nodes plus a dispose that runs the build's teardowns. The caller composes /
serializes the nodes; effects are NOT dispatched (server render is pure).
For persistent layouts, compose multiple renderNodes results before
serializeNodes so the layout/page trees are stitched at the slot position.
function renderNodes<S, M, E>(def: SignalComponentDef<S, M, E>, initialState: S | undefined, env: ServerDoc, contexts?: ReadonlyMap<symbol, unknown>): { nodes: readonly Node[]; dispose: () => void }
renderToString()
Render a signal component to an HTML string against the initial state (or a
provided override). env is a server DomEnv from @llui/dom/ssr/jsdom or
@llui/dom/ssr/linkedom.
function renderToString<S, M, E>(def: SignalComponentDef<S, M, E>, initialState: S | undefined, env: ServerDoc): string
rowHandle()
A ROW-rooted path handle (item/index inside an each/virtualEach row).
Identical to {@link pathHandle} but branded rowLocal — so a spec built from it
(or from .at/.map off it) reads the row ctx and is NOT rebased to
ctx.state. This is the emission target for the compiled each-arm prelude
(const item = rowHandle(getCtx, 'item')) and the authoring each/virtualEach
item/index handles.
function rowHandle<T>(get: () => unknown, base: string): SignalHandle<T>
script()
Add a <script> (external via src, or inline via body). Dedups by static
id or src; otherwise anonymous (keyed by stable construction order).
function script(attrs: ScriptAttrs = {}, body?: HeadValue<string>): Mountable
serializeNodes()
Serialize an array of (already-built) DOM nodes to an HTML string. Used by
adapters (@llui/vike) that compose layout + page node trees before one final
serialization pass.
function serializeNodes(nodes: readonly Node[]): string
show()
function show<T>(cond: Signal<T>, render: (narrowed: Signal<NonNullable<T>>) => Renderable, orElse?: () => Renderable, transition?: TransitionOptions): Mountable
signalBranch()
Discriminated-union render. Mounts the arm matching the discriminant's current value; swaps arms when it changes (the old arm unmounts, the new one mounts as a child scope). Same-value updates do NOT remount — the mounted arm's child scope handles its own inner reactivity. An absent arm renders nothing.
function signalBranch(disc: ShowCond, arms: Readonly<Record<string, () => Renderable>>, transition?: TransitionOptions): Mountable
signalEach()
Keyed list primitive. A structural binding gated on the list's deps (items
path + row-state paths); on change it reconciles by key. Each row is its OWN
signal scope mounted on a combined { item, state } context — so a row reacts
to its item AND to component state, with per-row, per-binding gating (a shared
state change fans out only to the row bindings that read it; item changes hit
only that row). Kept rows are mutated in place, never recreated.
Reorder is move-minimizing via a longest-increasing-subsequence pass over the
rows' previous DOM positions: only n − |LIS| rows move, so a 2-row swap is 2
DOM moves and a single removal is 0 — not the O(n) re-insert the naive cursor
walk degraded to (swap/remove were ~6×/4× slower than peer frameworks).
function signalEach<T>(source: EachSource<T>, key: (item: T) => string | number, renderRow: (getCtx: () => RowCtx<T>) => Renderable, extraDeps?: readonly string[], transition?: TransitionOptions): Mountable
signalEachDirect()
Direct-construction keyed list: same keyed reconcile as {@link signalEach}, but each row is built by a {@link RowFactory} (direct DOM + direct binding wiring) instead of running authoring helpers per row. The compiler-emitted fast path for lowerable rows; also usable hand-written.
function signalEachDirect<T>(source: EachSource<T>, key: (item: T) => string | number, rowFactory: RowFactory, extraDeps?: readonly string[], transition?: TransitionOptions): Mountable
signalForeign()
Imperative-subtree boundary. Declared state signals are materialized to
LiveSignals (peek + bind) and handed to mount, which builds a third-party
instance into the host element. The signals stay reactive: when a declared
input changes, its LiveSignal fires bound callbacks. unmount runs on the
owning component's dispose. Communicate OUT via send (closed over from the
view bag). The analyzer sees the declared deps; the imperative body is opaque.
function signalForeign<Inst, State extends Record<string, SignalSpec<unknown>>>(spec: ForeignSpec<Inst, State>): Mountable
signalLazy()
Load a signal component asynchronously. Renders fallback() immediately as
siblings of an anchor comment (built in the CURRENT build, so the fallback is
reactive). When loader() resolves, the fallback region is removed and the
loaded component is mounted via mountSignalComponent({ anchor, mode:'append' })
— reusing the anchor-mount infra (nodes inserted after the anchor, bracketed by
an llui-mount-end sentinel; its handle owns that region's update loop and
dispose). If the loader rejects, error(err) is swapped in (or nothing).
If the surrounding build is torn down before the loader settles, a cancelled
flag skips the deferred mount; any already-mounted child handle is disposed.
function signalLazy<LS = unknown, LM = unknown, LE = unknown>(opts: SignalLazyOptions<LS, LM, LE>): Mountable
signalShow()
Conditional render. Mounts render's content when the condition is truthy; if
an orElse arm is given, mounts it when falsy (otherwise nothing). The mounted
arm is its OWN scope that reads the owning component's state, registered as a
child of the owning scope — so while mounted it receives state updates (its
bindings re-run when THEIR deps change, not just when the condition flips).
Toggling the condition swaps arms; a same-truthiness update does NOT remount.
function signalShow(cond: ShowCond, render: () => Renderable, orElse?: () => Renderable, transition?: TransitionOptions): Mountable
signalText()
A reactive text node bound to a signal accessor. Returns a Mountable that
builds the text node and registers its binding when placed. componentRooted
(set by the authoring layer from the origin handle) drives correct row rebasing;
compiler-emitted calls omit it and rely on dep-string inference.
function signalText(produce: Producer, deps: readonly string[], componentRooted?: boolean): Mountable
signalUnsafeHtml()
Render a raw HTML string as live DOM nodes, inline between anchor comments (no
wrapper element). Reactive: when the bound string changes, the previously
inserted fragment is removed and the new HTML parsed in. The parsed nodes carry
NO reactive bindings — unsafeHtml is an escape hatch for pre-rendered markup
(markdown, syntax highlighting). The caller is responsible for trust/sanitization.
function signalUnsafeHtml(produce: Producer, deps: readonly string[], componentRooted?: boolean): Mountable
signalVirtualEach()
Virtualized keyed list — only the rows in the scroll viewport (+overscan) exist
in the DOM. A scroll container (fixed containerHeight, data-virtual-container)
holds an inner spacer (data-virtual-spacer) sized to the total height; each
visible row is absolutely positioned (translateY) at its cumulative offset.
On scroll the visible window is recomputed and rows are reconciled BY KEY using
the same per-row machinery as signalEach (per-row sub-build via runBuild
with inherit, a row scope mounted on a { item, state, index } ctx, teardowns
on removal). Rows scrolled out are disposed; rows scrolled in are built. The
window also recomputes when items changes (a spec gated on items.deps).
itemHeight is a uniform number (O(1) windowing) or a per-item function
(item, index) => number for variable-height rows (cumulative offsets via a
prefix sum, rebuilt when items changes). Heights come from the data —
measured/auto heights are not supported.
function signalVirtualEach<T>(spec: VirtualEachSpec<T>): Mountable
staticText()
A static text node.
function staticText(value: string): Mountable
style()
Add an inline <style> with css as its text content.
function style(css: HeadValue<string>, attrs: StyleAttrs = {}): Mountable
tagSend()
Library helper for *.connect implementations: tags an event
handler with the variants it dispatches at runtime, so the binding
registers them when the user spreads the bag onto an element.
Resolution rules — choose whichever is defined and non-empty:
send.__lluiVariants(translator pattern). When the user passed a compiler-tagged dispatch translator like(m) => dispatch({type: 'Auth/UserMenu'}),senditself carries the user-side variants the translator forwards. We surface those — the agent should see whatupdate()actually receives, not the library's internal Msg shape.libraryVariantsfallback. Whensendis the user's raw component send (no translator), the library's internal Msgs flow directly intoupdate(), so the library's own variants ARE the user variants. Library author hand-lists them once per handler. Returnsfnmutated (viaObject.assign) so the same reference remains identity-equal — important for downstream code that diffs handlers across re-bindings. @example
import { tagSend } from '@llui/dom'
export function connect<S>(get, send, opts) {
return {
trigger: {
onClick: tagSend(send, ['Open'], () => send({ type: 'open' })),
},
}
}
function tagSend<F extends (...args: never[]) => unknown>(send: unknown, libraryVariants: readonly string[], fn: F): F
text()
function text(value: Reactive<string | number>): Mountable
title()
Set the document <title>. Reactive when given a signal. Last writer in the
tree (deepest layout/page) wins; restored on unmount. Combine with
{@link titleTemplate}.
function title(value: HeadValue<string>): Mountable
titleTemplate()
A %s template the active {@link title} is interpolated into — e.g.
titleTemplate('%s · LLui') + title('Docs') → Docs · LLui. Applies only
while a title is set.
function titleTemplate(value: HeadValue<string>): Mountable
unsafeHtml()
Render a raw HTML string as live DOM nodes (escape hatch for pre-rendered
markup — markdown, syntax highlighting). Reactive on a Signal<string>; a
plain string renders once. The HTML is inserted as-is — the caller owns
trust/sanitization.
function unsafeHtml(value: Reactive<string>): Mountable
useContext()
Read the nearest provided value for context, or its default. Outside a
signal build (e.g. a unit test calling connect() directly) no provider can
exist, so the default is returned rather than throwing.
function useContext<T>(context: Context<T>): T
virtualEach()
Virtualized keyed list — only the rows in the scroll viewport (+overscan)
exist in the DOM. items is a signal handle (like each); the render callback
receives per-row item + index signal handles. itemHeight is either a
uniform number (O(1) windowing) or a per-item (item, index) => number for
variable-height rows (cumulative offsets via a prefix sum, rebuilt when items
changes). Heights come from the data — measured/auto heights are not supported.
function virtualEach<T>(opts: {
items: Signal<readonly T[]>
key: (item: T) => string | number
itemHeight: number | ((item: T, index: number) => number)
containerHeight: number
overscan?: number
class?: string
render: (item: Signal<T>, index: Signal<number>) => Renderable
}): Mountable
Types
AttrValue
export type AttrValue = Reactive<string | number | boolean | null | undefined>
ChildNode
A child slot: a lazy Mountable (everything LLui builds — elements, text, and
structural primitives — is a Mountable, materialized at placement), or a bare
string/number coerced to a static text node at append time (so div(['hi', 42])
works without an explicit text(...) — the same coercion every mainstream framework
does). There is no bare Node here: a node lives in one place, so exposing one would
reintroduce the silent double-placement footgun. Wrap raw DOM via foreign.
export type ChildNode = Mountable | string | number
ElProps
Props for an element helper. Well-known on* handlers (see {@link ElEventMap})
get their precise DOM event type, so onClick: (e) => e.clientX infers
e: MouseEvent with no annotation. Every other key — attributes, data-*,
aria-*, signals, and less-common events — is an {@link AttrValue} or a
loosely-typed handler via the index signature, which also lets connect()
part bags (with their own pre-typed handlers) spread in cleanly.
The handler index falls back to any ON PURPOSE: a stricter index type would
be a supertype of the precise on* handlers and reject them (function params
are contravariant), so the precise types live in the mapped half and the
index stays permissive.
export type ElProps = {
[K in keyof ElEventMap]?: (ev: ElEventMap[K]) => void
} & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: AttrValue | ((ev: any) => void) | undefined
}
EventHandler
export type EventHandler = (ev: Event) => void
HeadTarget
Where a head entry lands: a <head> element, an attribute on <html>/<body>,
or the (template-composed) <title>.
export type HeadTarget =
| { kind: 'element'; tag: string }
| { kind: 'attr'; on: 'html' | 'body'; name: string }
| { kind: 'title' }
| { kind: 'titleTemplate' }
HeadValue
A head value: a plain value (committed once) or a Signal (committed on
mount and on every change). Mirrors how foreign accepts handles-or-values.
export type HeadValue<T> = T | Signal<T>
MountTarget
Where a mountSignal call attaches its built nodes. A container element
(the common case — append, or replace its children on hydration) OR an
anchor comment, for adapters like @llui/vike that mount a nested layer as
siblings of a slot anchor without owning the parent element. The owned region
is bracketed by the anchor and a synthesized end sentinel; dispose() removes
exactly that region (leaving the anchor + outer siblings intact).
export type MountTarget =
| { container: Element; mode?: 'append' | 'replace' }
// `mode: 'replace'` (hydration) first removes any existing server region
// between the anchor and the next `llui-mount-end` sentinel, then mounts fresh
// — mirroring container hydration's atomic swap (no claim of server nodes).
| { anchor: Comment; mode?: 'append' | 'replace' }
PathValue
Resolve the value type at a dot-separated path of T, bubbling
nullability: once any segment introduces null/undefined (optional field,
nullable field, array index), it carries through to the result.
export type PathValue<T, S extends string> = [Extract<T, null | undefined>] extends [never]
? S extends `${infer Head}.${infer Tail}`
? PathValue<GetKey<T, Head>, Tail>
: GetKey<T, S>
: PathValue<NonNullable<T>, S> | Extract<T, null | undefined>
PropValue
export type PropValue = string | number | boolean | null | Reactive | EventHandler
Reactive
A reactive value in a slot: a signal of T, or a plain T.
export type Reactive<T> = Signal<T> | T
Renderable
The result of a render callback / view: lazy Mountables, materialized at
placement by populate/runBuild.
export type Renderable = readonly Mountable[]
RowFactory
Builds a fresh {@link DirectRow} (new nodes + binding closures) per row.
getCtx exposes the row's LIVE { item, state, index } ctx (the same box the
binding produce(ctx) reads), so a row's event-handler closures can read the
current row item at event time — onClick: () => send({ type: 'toggle', id: getCtx().item.id }) — the direct-path analogue of the render path's
pathHandle(getCtx, 'item'). Rows with no item-referencing handlers ignore it.
export type RowFactory = (doc: SignalDoc, getCtx: () => RowCtx<unknown>) => DirectRow
Send
export type Send<M> = (msg: M) => void
ServerDoc
A server DOM document: the node-factory subset the build needs. A DomEnv
from @llui/dom/ssr/jsdom or @llui/dom/ssr/linkedom satisfies it.
export type ServerDoc = SignalDoc
StateHandle
The bag's state is a Signal<S> so authored handler code reads it the same
way as the view (state.at('x').peek()). At runtime it's a read handle: .at
narrows, .peek reads the current value; .map is a view-build-time concept
and throws if reached on the handle.
export type StateHandle<S> = Signal<S>
ValidPath
The union of all valid dot-separated paths of T — both intermediate
(object) paths and leaf paths. Arrays contribute ${number} indices,
${number}.<sub> nested paths, and 'length'. Navigation descends through
nullable/optional fields (via NonNullable), bounded by {@link PathDepthBudget}.
export type ValidPath<T, D extends number = PathDepthBudget> = T extends null | undefined
? ValidPath<NonNullable<T>, D>
: T extends readonly (infer U)[]
? D extends 0
? `${number}` | 'length'
: `${number}` | `${number}.${ValidPath<U, DecrDepth[D]>}` | 'length'
: T extends object
? {
[K in keyof T & string]: NonNullable<T[K]> extends object
? D extends 0
? K
: K | `${K}.${ValidPath<NonNullable<T[K]>, DecrDepth[D]>}`
: K
}[keyof T & string]
: never
Interfaces
BaseAttrs
Attributes accepted by {@link base}.
export interface BaseAttrs {
href?: HeadValue<string>
target?: HeadValue<string>
}
BindingDebugInfo
export interface BindingDebugInfo {
index: number
mask: number
lastValue: unknown
kind: string
key: string | undefined
dead: boolean
perItem: boolean
}
BindingError
A binding-evaluation failure surfaced to a setOnBindingError hook. Shape
matches the agent's dispatch-envelope drain.errors entries.
export interface BindingError {
kind: string
key?: string
message: string
stack?: string
}
BindingLocation
export interface BindingLocation {
bindingIndex: number
kind: string
key: string | undefined
mask: number
lastValue: unknown
/** How the binding's node relates to the matched element. */
relation: 'self' | 'text-child' | 'comment-child'
}
BindingSpec
A reactive binding: the dependency paths it reads + an accessor (produce)
and a commit that applies the value. This is the compiler transform's output
target, and the contract a {@link DirectRow} (compiled each row) supplies.
export interface BindingSpec {
deps: readonly string[]
produce: Producer
commit: (value: unknown) => void
// A structural primitive's spec (show/branch/each): its `produce` is identity
// and `commit` reconciles arms/rows owning child scopes. Structural specs make
// themselves row-aware at build time (see `c.inRow`), so the enclosing `each`'s
// value-spec rebasing must SKIP them rather than rewrite their identity produce.
structural?: boolean
// Root discriminant for row rebasing — set from the ORIGIN handle, so row
// locality never depends on string-inferring `item`/`index`/`state` prefixes
// (which collide with a component-state field literally named that). `true` ⇒
// the produce reads the COMPONENT state (rebase to `ctx.state` inside a row);
// `false` ⇒ it already reads the row ctx (an item/index handle, or an
// already-rebased spec — leave it). `undefined` ⇒ a compiler-emitted spec with
// no handle origin: fall back to the legacy `isRowLocalDep` string inference
// (compiled rows use the `item.*`/`state.*` ctx convention, so this is sound).
componentRooted?: boolean
}
CollectedHead
The serialized output of a server render's head collection.
export interface CollectedHead {
/** `<head>` element markup (title/meta/link/…), each marked `data-llui-head`. */
head: string
/** Attribute string for `<html …>` (leading space included), already escaped. */
htmlAttrs: string
/** Attribute string for `<body …>` (leading space included), already escaped. */
bodyAttrs: string
/** Dedup keys present in `head` (e.g. `title`, `meta:name=description`). Used by
* {@link mergeStaticHead} to strip colliding tags from a static `+Head.ts`. */
keys: readonly string[]
}
CollectHeadSink
A server-side {@link HeadSink} that collects entries and serializes them. Seed
it via {@link HEAD_SINK} before rendering, then call serialize(env).
export interface CollectHeadSink extends HeadSink {
serialize(doc: SignalDoc): CollectedHead
}
ComponentBag
export interface ComponentBag<S, M> {
state: Signal<S>
send: (msg: M) => void
/** Coalesce a burst of `send`s into ONE reconcile (see the handle's `batch`).
* Reducers/effects still run per message; only the DOM commit is deferred to the
* outermost `batch` exit. Use it to drain a burst of dispatches (e.g. a stream
* frame) from a handler/subscription as a single re-render. */
batch: (fn: () => void) => void
}
ComponentInfo
export interface ComponentInfo {
name: string
file: string | null
line: number | null
/** Identifies which runtime mounted the component. */
runtime?: 'signal' | 'legacy'
}
Context
export interface Context<T> {
readonly id: symbol
readonly default: T
}
CoverageSnapshot
Per-variant Msg coverage tracker — dev-only.
Records each dispatched message's discriminant (or <non-discriminant>
for objects missing a type field) along with the message index it
fired at. Consumed by the llui_coverage MCP tool to surface untested
Msg variants: any variant declared in the compiled __msgSchema that
never fired in the current session shows up in neverFired.
Zero cost in production: installDevTools is the only caller, and it
never runs in prod builds. Hot path is one optional-chain read per
dispatched message (ci._coverage?.record(...)).
export interface CoverageSnapshot {
fired: Record<string, { count: number; lastIndex: number }>
neverFired: string[]
}
DirectRow
A compiler-emitted (or hand-written) direct each row: real DOM nodes built
with direct ops + binding specs wired by DIRECT node reference — bypassing the
authoring-helper / Mountable / populate / pathHandle machinery the
generic row path runs per row. The factory runs per row under the build ctx;
each spec's produce(ctx) reads the row ctx ({ item, state, index }) and its
commit writes straight to the located node. See
docs/proposals/v2-compiler/compiled-row-construction.md.
export interface DirectRow {
nodes: Node[]
bindings: readonly BindingSpec[]
}
DisposerEvent
Dev-only disposer log entry, emitted once per disposeLifetime call
when the owning component instance has an _disposerLog ring buffer
installed by installDevTools.
cause is set by the structural primitive (each / branch / child)
immediately before calling disposeLifetime. When no cause was
explicitly set, disposeLifetime falls back to 'component-unmount'.
'app-unmount' is reserved for the top-level mountApp teardown.
Used by the llui_disposer_log MCP tool to diagnose leaks on
structural transitions (e.g., branch swap that fails to release a
subscription registered in the old arm).
export interface DisposerEvent {
scopeId: string
cause:
| 'branch-swap'
| 'each-remove'
| 'show-hide'
| 'scope-rebuild'
| 'child-unmount'
| 'app-unmount'
| 'component-unmount'
timestamp: number
}
DomEnv
Minimal DOM surface that @llui/dom's internals depend on. Passed to
mountApp / hydrateSignalApp / renderToString as a context object so
the runtime never reaches for globalThis.document directly.
Why an injected shape instead of a global shim:
- Bundler-friendly. A Cloudflare Worker that imports
@llui/dom/ssr/linkedomreaches only linkedom via its module graph. Noawait import('jsdom')appears in reachable source, so rollup doesn't inline the 9 MiB jsdom bundle. - Concurrency-safe. Two
renderToStringcalls can pass different envs; no process-level singleton to collide on. - Strict-isolate safe. No
globalThis[key] = ...mutation — Cloudflare workerd and Deno strict modes forbid it. The surface is deliberately narrow: exactly the methods and constructors the runtime touches. Grepdocument\./instanceof (HTMLElement|Element|...)inside@llui/dom/srcfor the exhaustive set.
export interface DomEnv {
// ── Factories ────────────────────────────────────────────────────
createElement(tag: string): Element
createElementNS(ns: string, tag: string): Element
createTextNode(text: string): Text
createComment(text: string): Comment
createDocumentFragment(): DocumentFragment
/**
* Used by `each()`'s fast clear/bulk-remove paths to delete a range
* of siblings in one call. SSR adapters that don't need those paths
* (jsdom + linkedom both do) can stub — the runtime tolerates a
* missing range during SSR render, which never hits the bulk paths.
*/
createRange(): Range
// ── Node / element constructors ─────────────────────────────────
// Exposed for `instanceof` checks in binding targeting + for any
// rare site that needs to construct a node type directly.
readonly Element: typeof Element
readonly Node: typeof Node
readonly Text: typeof Text
readonly Comment: typeof Comment
readonly DocumentFragment: typeof DocumentFragment
readonly HTMLElement: typeof HTMLElement
readonly HTMLTemplateElement: typeof HTMLTemplateElement
readonly ShadowRoot: typeof ShadowRoot
// ── Event constructor ───────────────────────────────────────────
readonly MouseEvent: typeof MouseEvent
/**
* Parse an HTML fragment string into a `DocumentFragment`. Used by
* `unsafeHtml()`. Browsers and jsdom parse via template-element
* innerHTML; linkedom has its own fragment parser. Adapter chooses
* the right mechanism.
*/
parseHtmlFragment(html: string): DocumentFragment
/**
* Resolve a CSS selector against the env's root document. Used by
* `portal()` to locate its target when `opts.target` is a string.
*
* Returns `null` when the selector doesn't match — portal callers
* treat a null target as a no-op (render nothing), so adapters on
* runtimes where no real document exists (detached linkedom, empty
* shadow root, etc.) can safely return `null` here.
*
* Required — making this mandatory on the interface means a custom
* env that forgets to wire up selector resolution fails compile
* instead of silently falling back to `globalThis.document` at
* render time (which would crash under Cloudflare Workers + other
* strict-isolate runtimes). The three first-party envs
* (`browserEnv`, `jsdomEnv`, `linkedomEnv`) all implement it.
*/
querySelector(selector: string): Element | null
/**
* @internal Lets hot-path code (e.g. `el-split.ts`'s template-clone)
* skip env indirection when the env wraps the browser globals. Only
* set by `browserEnv()`.
*/
readonly isBrowser?: boolean
}
EachDiff
Per-each-block reconciliation diff, recorded once per update that
mutates an each() block's key set. Dev-only — populated when
installDevTools has initialized an _eachDiffLog on the instance.
updateIndex correlates with the message-history index recorded by
devtools.ts so tools can join diffs back to the message that caused
them. eachSiteId identifies the each() call site stably across
updates (currently derived from the block's index in the instance's
structuralBlocks array at creation time).
export interface EachDiff {
/**
* Message-history index at the time the diff was emitted. When messages are
* batched (multiple send() calls coalescing into one microtask), this is
* the index of the LAST message in the batch — not necessarily the one that
* caused the structural change. For per-message correlation, use
* getMessageHistory with this index as an upper bound.
*/
updateIndex: number
/**
* Stable-ish identifier for the each() call site. Currently derived from the
* position of the block in `ComponentInstance.structuralBlocks` at the moment
* of registration, formatted as `each#${N}`.
*
* Caveats for consumers:
* - The counter includes ALL structural blocks (branches, shows, portals,
* eaches), not just eaches. So `each#3` means "the 4th structural block",
* not "the 4th each".
* - Blocks registered inside a `branch` arm that switches away are spliced
* out; a subsequent each registration can reuse the same N.
* - Across HMR reloads the ID may drift if the view's structural-block
* order changed.
*
* For precise correlation across updates, pair with `updateIndex` and the
* enclosing component's state at that index (retrievable via
* getMessageHistory).
*/
eachSiteId: string
added: string[]
removed: string[]
moved: Array<{ key: string; from: number; to: number }>
reused: string[]
}
EachSource
Items source for signalEach: an accessor reading the array out of the
component state, plus the dep paths the list depends on — the items path AND
any component-state paths the rows read (so the list reconciles on either).
export interface EachSource<T> {
items: (state: unknown) => readonly T[]
deps: readonly string[]
/** See {@link BindingSpec.componentRooted}: `true` when the items accessor reads
* the COMPONENT state (so a nested each reads `ctx.state`, not the enclosing row
* ctx). Set by the authoring layer from the items handle; unbranded → inference. */
componentRooted?: boolean
}
EffectApi
export interface EffectApi<S, M> {
send: (msg: M) => void
state: Signal<S>
/** Coalesce a burst of `send`s into ONE reconcile (see {@link ComponentBag.batch}). */
batch: (fn: () => void) => void
/** This mount's lifecycle {@link AbortSignal}. Aborted exactly once, on THIS
* mount's `dispose()`. Each mount of a definition owns a distinct signal, so an
* effect handler can key per-mount resources off it — pass it to `fetch`, or
* `signal.addEventListener('abort', cleanup)` — and disposing one mount never
* aborts a concurrent mount of the same definition. Prefer this over a
* definition-level controller for anything whose lifetime is the mount's. */
signal: AbortSignal
}
EffectMatch
Match predicate for the mock registry. All provided fields must match for the mock to fire:
type: exact-match against the effect'stypediscriminant.payloadPath: dotted path into the effect object (e.g.'url'or'body.key'). When present withoutpayloadEquals, presence of the path is sufficient.payloadEquals: strict (===) equality check atpayloadPath. An empty match (no fields) matches every effect — callers should set at leasttypeto avoid accidental catch-all.
export interface EffectMatch {
type?: string
payloadPath?: string
payloadEquals?: unknown
}
EffectTimelineEntry
export interface EffectTimelineEntry {
effectId: string
type: string
phase: 'dispatched' | 'in-flight' | 'resolved' | 'resolved-mocked' | 'cancelled'
timestamp: number
/** Populated on `resolved` / `resolved-mocked` / `cancelled` entries; undefined on open phases. */
durationMs?: number
}
ElementReport
export interface ElementReport {
selector: string
tagName: string
attributes: Record<string, string>
classes: string[]
dataset: Record<string, string>
text: string
computed: {
display: string
visibility: string
position: string
width: number
height: number
}
boundingBox: { x: number; y: number; width: number; height: number }
bindings: Array<{
bindingIndex: number
kind: string
mask: number
lastValue: unknown
relation: 'self' | 'text-child' | 'comment-child'
}>
}
ForeignSpec
export interface ForeignSpec<Inst, State extends Record<string, SignalSpec<unknown>>> {
/** host element tag (default 'div') */
tag?: string
/** declared reactive inputs — materialized to LiveSignals for `mount` */
state?: State
/** build the imperative instance into the host element */
mount: (args: {
el: Element
state: { [K in keyof State]: LiveSignal<State[K] extends SignalSpec<infer T> ? T : unknown> }
}) => Inst
/** tear down the instance (runs on the owning component's dispose) */
unmount?: (instance: Inst) => void
}
HeadController
A single registered writer's handle: the binding sets its value(s) here;
teardown releases it.
export interface HeadController {
set(attrs: Record<string, unknown>, text?: string): void
release(): void
}
HeadSink
The coordinator a head primitive commits through. One per app document (client) or one per render (server collector).
export interface HeadSink {
register(key: string, target: HeadTarget): HeadController
}
HydrationDivergence
export interface HydrationDivergence {
path: string
kind: 'attribute' | 'text' | 'structural'
server: unknown
client: unknown
}
LifetimeNode
Lifetime-tree node for the debug/agent surface. A serialized snapshot of the live scope tree — the signal devtools surface and MCP tools read this shape to render scope lifecycle.
export interface LifetimeNode {
scopeId: string
kind: 'root' | 'show' | 'each' | 'branch' | 'scope' | 'child' | 'portal' | 'foreign'
active: boolean
children: LifetimeNode[]
}
LinkAttrs
Attributes accepted by {@link link}.
export interface LinkAttrs {
rel?: string
href?: HeadValue<string>
[attr: string]: HeadValue<string> | undefined
}
LiveSignal
A materialized signal handed to imperative code at the foreign boundary.
Minimal on purpose — all derivation stays in the declarative state:
declaration, so this is a read+subscribe handle only.
peek()— one-shot, non-reactive read (same verb as {@link Signal}).bind(cb)— firescbsynchronously with the current value, then on every change; returns an unsubscribe. Mount-timebinds auto-dispose on unmount. Deliberately noon(event-listener vocabulary trains a redundant peek-then-subscribe), no change-only mode, and noat/map/derived.
export interface LiveSignal<T> {
peek(): T
bind(cb: (value: T) => void): () => void
}
LluiDebugAPI
The relay-callable debug surface of a mounted LLui component.
Required methods are implemented by every runtime (and by
installSignalDebug). Optional methods are binding/scope/effect
introspection that only the legacy runtime provides — callers must
feature-detect and degrade when they are absent.
export interface LluiDebugAPI {
// ── Core (always implemented) ──────────────────────────────────
getState(): unknown
send(msg: unknown): void
flush(): void
getMessageHistory(opts?: { since?: number; limit?: number }): MessageRecord[]
evalUpdate(msg: unknown): { state: unknown; effects: unknown[] }
exportTrace(): {
lluiTrace: 1
component: string
generatedBy: string
timestamp: string
entries: Array<{ msg: unknown; expectedState: unknown; expectedEffects: unknown[] }>
}
clearLog(): void
validateMessage(msg: unknown): ValidationError[] | null
searchState(query: string): unknown
getMessageSchema(): MessageSchemaInfo | object | null
getStateSchema(): object | null
getEffectSchema(): object | null
getComponentInfo(): ComponentInfo
snapshotState(): unknown
restoreState(snap: unknown): void
// ── Binding / scope introspection (legacy-only; optional) ──────
getBindings?(): BindingDebugInfo[]
whyDidUpdate?(bindingIndex: number): UpdateExplanation
getMaskLegend?(): Record<string, number> | null
decodeMask?(mask: number): string[]
getBindingsFor?(selector: string): BindingLocation[]
getBindingGraph?(): Array<{ statePath: string; bindingIndices: number[] }>
getBindingSource?(bindingIndex: number): { file: string; line: number; column: number } | null
forceRerender?(): { changedBindings: number[] }
getEachDiff?(sinceIndex?: number): EachDiff[]
getScopeTree?(opts?: { depth?: number; scopeId?: string }): LifetimeNode
getDisposerLog?(limit?: number): DisposerEvent[]
// ── DOM inspection (legacy-only; optional) ─────────────────────
inspectElement?(selector: string): ElementReport | null
getRenderedHtml?(selector?: string, maxlength?: number): string
dispatchDomEvent?(
selector: string,
type: string,
init?: EventInit,
): {
dispatched: boolean
messagesProducedIndices: number[]
resultingState: unknown | null
}
getFocus?(): {
selector: string | null
tagName: string | null
selectionStart: number | null
selectionEnd: number | null
}
getHydrationReport?(): HydrationDivergence[]
// ── Effect introspection (legacy-only; optional) ───────────────
getPendingEffects?(): PendingEffect[]
getEffectTimeline?(limit?: number): EffectTimelineEntry[]
mockEffect?(
match: EffectMatch,
response: unknown,
opts?: { persist?: boolean },
): { mockId: string }
resolveEffect?(effectId: string, response: unknown): { resolved: boolean }
// ── Time-travel / coverage / eval (legacy-only; optional) ──────
stepBack?(n: number, mode: 'pure' | 'live'): { state: unknown; rewindDepth: number }
getCoverage?(): CoverageSnapshot
getCompiledSource?(viewFn?: string): { pre: string; post: string } | null
getMsgMaskMap?(): Record<string, number> | null
evalInPage?(code: string): {
result: unknown | { error: string }
sideEffects: {
stateChanged: StateDiff | null
newHistoryEntries: number
newPendingEffects: PendingEffect[]
dirtyBindingIndices: number[]
}
}
}
MessageRecord
export interface MessageRecord {
index: number
timestamp: number
msg: unknown
stateBefore: unknown
stateAfter: unknown
effects: unknown[]
/** Present only on the legacy runtime, which computes a dirty mask per update. */
dirtyMask?: number
}
MessageSchemaInfo
export interface MessageSchemaInfo {
discriminant: string
variants: Record<string, Record<string, unknown>>
}
MetaAttrs
Attributes accepted by {@link meta}. Identity attrs (name/property/
httpEquiv/charset) should be static so the entry can dedup.
export interface MetaAttrs {
name?: string
property?: string
httpEquiv?: string
charset?: string
content?: HeadValue<string>
[attr: string]: HeadValue<string> | undefined
}
Mountable
A lazy node description: mount() builds the live node (and registers its
bindings into the active build) at placement time. Everything LLui builds —
elements, text, and structural primitives — is a Mountable, materialized
where it is placed (see populate/runBuild).
export interface Mountable {
readonly [MOUNTABLE]: true
mount(): Node
}
MountSignalOptions
Options for mountSignalComponent.
export interface MountSignalOptions<S> {
/** Hydrate over server-rendered DOM instead of a fresh mount: seed the loop
* with `serverState` (what the server rendered with) and atomically REPLACE the
* server HTML with the freshly-built client tree. init()'s effects are skipped
* by default (the server pass already ran them) — opt back in with
* `runInitEffects` for init()s gated to no-op on the server. */
hydrate?: { serverState: S; runInitEffects?: boolean }
/** Seed state to mount with instead of `init()`'s result (adapters that derive
* the seed externally, e.g. per-route data). init() still runs so its effects
* are captured; only the returned state is overridden. Ignored when hydrating
* (use `hydrate.serverState` there). */
initialState?: S
/** Context values to expose at the root of this build (see `runBuild`'s
* `seedContexts`). `@llui/vike` replays a layout's in-scope contexts here so a
* nested page reads providers that live above its slot in a SEPARATE build. */
contexts?: ReadonlyMap<symbol, unknown>
/** Commit scheduling. `'sync'` (the default) commits the DOM + notifies
* subscribers inside every top-level `send` — the synchronous contract.
* `'raf'` is the OPT-IN streaming/burst fast path: reducers and effects
* still run synchronously per send (state and `getState()` advance
* immediately — the data contract holds), but the DOM commit + subscriber
* notification coalesce to ONE reconcile per animation frame (microtask
* fallback where rAF doesn't exist: SSR, plain jsdom, the headless agent).
* The DOM therefore lags state by up to a frame; `handle.flush()` forces
* the pending commit synchronously (tests, the agent protocol, a
* read-after-write). Measured on the ticker suite's 1k-send burst:
* 14.1ms per-send vs 5.9ms coalesced (hand-written-vanilla parity). */
scheduler?: 'sync' | 'raf'
/** Register this component in the global devtools registry
* (`__lluiComponents` / `__lluiDebug`). Default `true` in dev. Set `false`
* for self-introspecting dev tooling (e.g. an in-app debug HUD authored with
* LLui) so it doesn't pollute the host app's component list that external
* tools — the MCP server, agent bridge, debug-collector — read. */
devtools?: boolean
}
PendingEffect
export interface PendingEffect {
id: string
type: string
dispatchedAt: number
status: 'queued' | 'in-flight'
payload: unknown
}
RowCtx
The per-row context a row scope mounts on: its item plus the current
component state. Row bindings read ctx.item.* (dep item.*) and
ctx.state.* (dep state.*) — so a row can react to BOTH its own item and
the component state (e.g. a shared display mode).
export interface RowCtx<T> {
item: T
state: unknown
/** the row's current position (dep `index`) — for runtime `each` index handles */
index: number
}
ScriptAttrs
Attributes accepted by {@link script}.
export interface ScriptAttrs {
src?: HeadValue<string>
type?: HeadValue<string>
async?: HeadValue<boolean>
defer?: HeadValue<boolean>
id?: string
[attr: string]: HeadValue<string | boolean> | undefined
}
ShowCond
Condition source for signalShow / discriminant source for signalBranch: an
accessor plus its dep paths.
export interface ShowCond {
produce: (state: unknown) => unknown
deps: readonly string[]
/** See {@link BindingSpec.componentRooted}: `true` when the condition reads the
* COMPONENT state (so inside a row it's fed `ctx.state`, not the combined ctx).
* Set by the authoring layer from the cond handle; unbranded → string inference. */
componentRooted?: boolean
}
Signal
A reactive view of a value of type T. Three methods, the entire reactive
vocabulary alongside derived:
at(path)— slice into a sub-signal via a statically-typed dot path.map(fn)— transform into a derived signal (single source).peek()— one-shot, non-reactive read (handlers / effects / lifecycle).
export interface Signal<T> {
/**
* Slice into a sub-signal via a statically-typed dot path
* (`state.at('user.profile.name')`). The path is validated and the result type
* resolved at compile time.
*
* **Depth limit.** `ValidPath<T>` enumerates the union of *every* valid dotted
* path of `T` (to validate the argument and power autocomplete). That union
* grows multiplicatively with the state's width × depth, so on a large /
* deeply-nested `T` it can exceed TypeScript's instantiation limit and surface
* as `TS2589: Type instantiation is excessively deep`. The cost comes from the
* *whole state shape*, not the single path you wrote. If you hit it, reach for
* `.map(s => s.deep.path)` instead — a `.map()` derive sidesteps path typing
* entirely (it reads the whole slice; the runtime still gates it correctly) and
* is the supported escape hatch for very deep paths.
*/
at<P extends ValidPath<T>>(path: P): Signal<PathValue<T, P>>
map<U>(fn: (value: T) => U): MappedSignal<U>
peek(): T
}
SignalComponentDef
export interface SignalComponentDef<S, M, E = never> {
/** optional component name (for the debug registry / agent identity) */
readonly name?: string
/** initial state, optionally with initial effects */
init: () => S | [S, E[]]
/** pure reducer; returns the next state, optionally with effects. A bare `S`
* (non-tuple) return is accepted for convenience. */
update: (state: S, msg: M) => [S, E[]] | S
/** build the view once; reactive reads are signal bindings (they don't close
* over `state`). The bag's `state` handle is for handlers/effects. */
view: (bag: ComponentBag<S, M>) => Renderable
/** handle an effect; may return a cleanup function */
onEffect?: (effect: E, api: EffectApi<S, M>) => void | (() => void)
// ── Compiler-injected introspection metadata (see @llui/compiler signals
// transform). Optional — present only in dev / agent builds. Read by the
// agent-client pairing path and the (signal) debug surface. ──
/** discriminated-union schema of Msg ({ discriminant, variants }) */
readonly __msgSchema?: object
/** discriminated-union schema of Effect */
readonly __effectSchema?: object
/** state shape schema */
readonly __stateSchema?: object
/** per-message JSDoc annotations (intent, affordability, …) */
readonly __msgAnnotations?: Record<string, unknown>
/** stable hash of the schemas, for hot-reload schema-change detection */
readonly __schemaHash?: string
/** dev-only source location */
readonly __componentMeta?: { file: string; line: number }
}
SignalComponentHandle
export interface SignalComponentHandle<S, M> {
send(msg: M): void
/** Coalesce a burst of `send`s into ONE reconcile + commit. Every message's
* reducer still runs in order (state advances message-by-message, effects fire
* per message), but the DOM reconcile + subscriber notification are deferred to
* a single pass against the FINAL state when the outermost `batch` returns.
* For N synchronous sends this turns N reconciles into 1 — the streaming /
* bulk-dispatch fast path (e.g. draining a websocket frame of ticks). State is
* applied by the time `batch` returns, so the synchronous-`send` contract holds
* at the batch boundary. Nested `batch` calls flush only at the outermost exit. */
batch(fn: () => void): void
getState(): S
/** no-op: signal `send` applies updates synchronously (kept for harness/agent
* parity with the legacy handle). */
flush(): void
/** run all pending effect cleanups (subscriptions etc.) */
dispose(): void
/** Register a listener called synchronously after every update cycle that
* changes state, with the new state. Returns an unsubscribe. No-op after
* dispose. Backs the agent protocol's state-update frames. */
subscribe(listener: (state: S) => void): () => void
/** Run the reducer in isolation against the current state — `{state, effects}`
* with no commit/flush/effect dispatch. Backs the agent's `would_dispatch`. */
runReducer(msg: M): { state: S; effects: unknown[] } | null
/** Snapshot the Msg variants dispatchable from currently-rendered UI (live
* `tagSend` registrations). Backs the agent's `list_actions`. */
getBindingDescriptors(): Array<{ variant: string }>
/** Hot-swap the reducer (and optionally onEffect) without rebuilding the DOM —
* the HMR escape hatch for pure update.ts edits. State-type erased at this
* boundary (`unknown`) so the handle stays assignable across state types. */
swapUpdate(
newUpdate: (state: unknown, msg: unknown) => [unknown, unknown[]] | unknown,
newOnEffect?: unknown,
): void
/** Install a hook called when a binding accessor throws during the update
* cycle; the runtime leaves the binding's DOM at its prior value and continues
* with siblings. Backs the agent's dispatch-envelope `drain.errors`. */
setOnBindingError(hook: ((e: BindingError) => void) | null): void
}
SignalComponentSpec
export interface SignalComponentSpec<S, M, E = never> {
/** optional component name (debug registry / agent identity) */
name?: string
init: () => S | [S, E[]]
update: (state: S, msg: M) => [S, E[]] | S
view: (bag: SignalViewBag<S, M>) => Renderable
onEffect?: (
effect: E,
api: {
send: Send<M>
state: Signal<S>
batch: (fn: () => void) => void
/** This mount's lifecycle {@link AbortSignal} — aborted on dispose. */
signal: AbortSignal
},
) => void | (() => void)
}
SignalDebugHooks
Everything the signal debug API needs from a mounted component. Supplied by mountSignalComponent; keeps this module decoupled from the mount internals.
export interface SignalDebugHooks {
name: string
getState: () => unknown
/** replace state and re-render (restore / time-travel) */
setState: (s: unknown) => void
send: (msg: unknown) => void
/** pure reducer, normalized to [state, effects] (for evalUpdate / dry-run) */
pureUpdate: (s: unknown, msg: unknown) => [unknown, unknown[]]
/** captured message log (newest last); installSignalDebug reads it live */
history: readonly SignalMessageRecord[]
clearHistory: () => void
msgSchema?: object
stateSchema?: object
effectSchema?: object
componentMeta?: { file: string; line: number }
}
SignalHandle
A runtime Signal: the read surface PLUS the binding info needed to build a
reactive slot at runtime (view-helper composition).
export interface SignalHandle<T> extends Signal<T> {
readonly [SIGNAL]: true
/** resolve the value from the binding's state (component or row ctx) */
readonly produce: (state: unknown) => T
/** dependency paths into the binding's state */
readonly deps: readonly string[]
/** Root discriminant for row rebasing. `true` ⇒ this handle reads the ROW ctx
* (an `item`/`index` handle from `rowHandle`, or a row-aware `derived`); `false`
* (or absent) ⇒ it reads the COMPONENT state and must be rebased to `ctx.state`
* when placed inside an `each` row. Set at construction from the getter's origin,
* so locality never depends on string-inferring a `state`/`item`/`index` field
* name (which collides with a component field literally named that). */
readonly rowLocal?: boolean
}
SignalLazyOptions
export interface SignalLazyOptions<LS = unknown, LM = unknown, LE = unknown> {
/** async loader — typically `() => import('./Chart').then(m => m.default)`. The
* loaded component's S/M/E are inferred, so `initialState` is typed and no cast
* is needed at the call site. */
loader: () => Promise<SignalComponentDef<LS, LM, LE>>
/** nodes rendered (reactively, in the current build) while loading */
fallback: () => Renderable
/** nodes rendered if the loader rejects (nothing if omitted) */
error?: (err: Error) => Renderable
/** seed state for the loaded component, overriding its `init()` result */
initialState?: LS
}
SignalMessageRecord
export interface SignalMessageRecord {
index: number
timestamp: number
msg: unknown
stateBefore: unknown
stateAfter: unknown
effects: unknown[]
}
SignalMount
export interface SignalMount {
/** apply a new state; only bindings whose deps changed re-run and commit. */
update(next: unknown): void
/** run teardowns (foreign unmount, subscriptions). */
dispose(): void
/** live agent-affordance variants (tagged-send handlers currently mounted). */
getDescriptors(): Array<{ variant: string }>
}
SignalSpec
A declared reactive input to foreign: an accessor + its dep paths.
export interface SignalSpec<T> {
produce: (state: unknown) => T
deps: readonly string[]
}
SignalViewBag
export interface SignalViewBag<S, M> {
state: Signal<S>
send: Send<M>
/** Coalesce a burst of `send`s into ONE reconcile (see the handle's `batch`). */
batch: (fn: () => void) => void
}
StateDiff
export interface StateDiff {
added: Record<string, unknown>
removed: Record<string, unknown>
changed: Record<string, { from: unknown; to: unknown }>
}
StyleAttrs
Attributes accepted by {@link style} / {@link script}. A static id keys the
tag for dedup + SSR-hydration adoption; without one the tag is anonymous (no
dedup, keyed by stable construction order).
export interface StyleAttrs {
id?: string
media?: HeadValue<string>
[attr: string]: HeadValue<string> | undefined
}
TransitionOptions
Enter/leave/cross transition hooks shared by the animation/transition
helpers (@llui/transitions) and the structural primitives that
accept them. Runtime-agnostic — operates on raw DOM Nodes.
export interface TransitionOptions {
enter?: (nodes: Node[]) => void | Promise<void>
leave?: (nodes: Node[]) => void | Promise<void>
onTransition?: (ctx: { entering: Node[]; leaving: Node[]; parent: Node }) => void | Promise<void>
}
UpdateExplanation
export interface UpdateExplanation {
bindingIndex: number
bindingMask: number
lastDirtyMask: number
matched: boolean
accessorResult: unknown
lastValue: unknown
changed: boolean
}
ValidationError
export interface ValidationError {
path: string
message: string
/** Set by the legacy validator; the signal validator omits these. */
expected?: string
received?: string
/**
* A complete, valid example message — attached to the FIRST error so an LLM
* can construct the corrected message in one shot instead of inferring the
* shape from the schema. Built for the targeted variant (or a representative
* variant when the variant is unknown/missing), reusing any valid fields the
* caller already supplied.
*/
example?: Record<string, unknown>
}
VirtualEachSpec
export interface VirtualEachSpec<T> extends EachSource<T> {
key: (item: T) => string | number
/** Row height in pixels. A `number` is a uniform fixed height (O(1) windowing);
* a function returns a per-item height, letting rows vary — the window, spacer,
* and row offsets are computed from cumulative heights (prefix sums, rebuilt when
* `items` changes). Heights must be known from the data; measured/auto heights
* are not supported. */
itemHeight: number | ((item: T, index: number) => number)
/** scroll-container height in pixels */
containerHeight: number
/** extra rows rendered above/below the viewport (default 3) */
overscan?: number
/** optional class on the scroll container */
class?: string
/** Additional COMPONENT-STATE dep paths the rows read, merged into the
* structural binding's deps so a state-only change (items unchanged) still fires
* the reconcile and refreshes visible rows. The authoring `virtualEach` passes
* `['']` (whole state); a compiled tier could pass precise paths. Without it, a
* row reading component state was frozen out of state-only changes (stale DOM). */
extraDeps?: readonly string[]
/** build a row; `getCtx` exposes the row's live `{ item, state, index }` ctx
* (same shape as `signalEach`) for runtime item/index handles. */
renderRow: (getCtx: () => RowCtx<T>) => Renderable
}
Constants
a
const a
article
const article
aside
const aside
blockquote
const blockquote
br
const br
button
const button
canvas
const canvas
caption
const caption
circle
const circle
code
const code
dd
const dd
details
const details
div
const div
dl
const dl
dt
const dt
ellipse
const ellipse
em
const em
fieldset
const fieldset
figcaption
const figcaption
figure
const figure
footer
const footer
form
const form
g
const g
h1
const h1
h2
const h2
h3
const h3
h4
const h4
h5
const h5
h6
const h6
HEAD_SINK
Context carrying the active sink. Default null → the client per-document
fallback; SSR / explicit coordination seed a sink here.
const HEAD_SINK: Context<HeadSink | null>
header
const header
hr
const hr
img
const img
input
const input
label
const label
legend
const legend
li
const li
line
const line
main
const main
nav
const nav
ol
const ol
optgroup
const optgroup
option
const option
p
const p
path
const path
polygon
const polygon
polyline
const polyline
pre
const pre
rect
const rect
section
const section
select
const select
small
const small
span
const span
strong
const strong
summary
const summary
svg
const svg
svgText
const svgText
table
const table
tbody
const tbody
td
const td
textarea
const textarea
th
const th
thead
const thead
time
const time
tr
const tr
ul
const ul