Twenty components. Containers hold children; the rest are leaves (with
noted exceptions). Every component also takes every
shared prop — layout, style, state, interaction,
motion.
Containers
col / row
Flex column / row.
| Prop | Values |
|---|---|
gap | xs s m l or px |
align | start center end baseline stretch |
justify | start center end between evenly |
{ type = "col", gap = 6, align = "center", children = { ... } }
box
A flex container (same gap/align/justify) meant to be painted —
give it bg, radius, ring, border. Dial faces, presence dots, and
panels are boxes.
{ type = "box", w = 44, h = 44, radius = 999, bg = "deep", border = "rim-strong" }
grid
CSS grid. cols (1–12), gap (column px), row_gap (px), align,
justify. Fixed children or a repeater — bind (array) + item +
max (default 64) — never both. The date calendar is 38 cells of one
repeater.
{ type = "grid", cols = 7, row_gap = 1, bind = "cells", max = 44, item = {
type = "text", bind = "d", size = 10, align = "center",
} }
frame
Absolute positioning. Every child must carry at = {x, y} or
at = {x, y, w, h} in design px. A child with anchor = "center" treats
its at as the center point. This is how numerals sit on a dial and badges
sit on avatars.
{ type = "frame", w = 26, h = 26, children = {
{ type = "image", at = { 2, 2, 22, 22 }, radius = 999, bind = "avatar" },
{ type = "box", at = { 16, 16, 9, 9 }, radius = 999, bg = "live",
ring = { 2, "island" } },
} }
spacer
An empty filler. Pair with flex (or a show token) to shove neighbours
apart.
{ type = "spacer", flex = 1 }
text
The workhorse. bind/value is the string; role picks a type-ramp class
(title, value, h2, huge, label, body…); explicit metrics
override the role.
| Prop | Values |
|---|---|
size / weight | 6–72 px / 100–900 |
tracking / line | -0.1–0.3 em / 6–100 px |
lines | 1–4 — clamps and ellipsizes |
align / caps | left center right / uppercase |
prefix / suffix | glued literal text |
map | a value → string lookup table |
{ type = "text", bind = "temp", suffix = "°", size = 25, weight = 700 }
{ type = "text", bind = "k", map = { hdr = "—", day = "" } }
icon
A Phosphor glyph by name. bind/value = glyph; set = "duotone" draws
the two-tone face (default is the line icon); size = xs s m l or
px (6–72); color.
{ type = "icon", value = "gear-six", set = "duotone", size = "s", color = "half" }
image
A bound (or static) picture — album art, avatars, app tiles. bind/value
= URL (https://, file://, data:); fit = cover | contain. It’s a
container of one fallback child (a glyph or monogram) shown while
imageless.
Loading is preload-and-retry: the old image holds until the new one
decodes; retries run ~15 s with cache-busters. A declared bg gradient
shows through while empty, and a viz with palette = "art" in the same
widget wears the loaded image’s extracted palette.
{ type = "image", bind = "artUrl", fit = "cover", w = 100, h = 100, radius = 25,
flip = "media-art", children = { note_glyph } }
shape
Retained vector art from sanitized SVG paths. vb = {w, h} (viewBox,
required). Either static paths (1–128) or a named shapes set (≤16)
picked by bind/value. Each path: d (path data, ≤2000 chars), fill,
stroke, width (0–10), cap, join. fill = "current" follows the
node’s color.
-- static glyph
{ type = "shape", vb = { 24, 24 }, w = 21, h = 21,
paths = { { d = "M8 5v14l11-7z", fill = "current" } } }
-- data-picked from a set (the weather icon family)
{ type = "shape", bind = "icon", shapes = WX, vb = { 64, 44 }, w = 34, h = 23 }
bar
Horizontal progress / seek. bind = fraction 0–1; negative =
indeterminate shimmer.
| Prop | Meaning |
|---|---|
height | px; also sets the radius |
track / fill | colors |
hot_at | fraction threshold that flips the fill to its hot look |
snap | fast width transitions for drag-rate feeds (the volume HUD) |
knob | a draggable dot |
seek | drag scrubs locally; ONE command fires on release with {frac} |
{ type = "bar", bind = "posFrac", height = 5, track = "well", knob = true,
seek = true, action = "seek:{frac}" }
ring
Circular progress, a container of centered children. style = conic (a
conic sweep — the pomodoro) or stroke (an SVG dial — downloads).
size/thickness (px), color (progress; defaults to the node’s color
so style_when recolors it live), track, cap. bind = fraction 0–1;
negative = indeterminate spin.
{ type = "ring", bind = "frac", style = "conic", size = 88, thickness = 5,
color = "accent-warm", children = {
{ type = "text", bind = "mmss", size = 21, weight = 700 },
} }
ticker
Live time text that ticks view-side — the interval dies with the node, so there are no Lua wakeups.
| Mode | bind | format |
|---|---|---|
clock | { off = <UTC-offset-min>, h24 = <bool> } or a plain offset | h:mm ap :ss |
elapsed | wall-epoch ms | m:ss |
countdown | wall-epoch ms | m:ss |
Optional h24_bind re-reads the format preference live; size, weight,
role, caps as on text.
{ type = "ticker", bind = "pill", mode = "clock", format = "h:mm", size = 17 }
{ type = "ticker", bind = "started", mode = "elapsed", size = 10 } -- call timer
viz
An N-bar audio visualizer. bind = an array of levels 0–1.
| Prop | Default | Meaning |
|---|---|---|
bars | 5 | 1–32 |
bar_w / gap | 4 / 1 | px |
floor | 0.18 | minimum level |
grow | scale or height | |
gradient | 2–4 fixed vertical stops | |
palette | "art" — wears the nearest image’s extracted palette |
{ type = "viz", bind = "bars", bars = 5, palette = "art", h = 20 }
spark
A mini line chart with a soft area fade (96×30). bind = a points array,
or { points = {...}, up = <bool> } where up picks stroke vs
stroke_down — the green/red financial defaults. area = false drops the
fade.
{ type = "spark", bind = "series", stroke = "live", stroke_down = "red" }
gauge
The live seconds scale (the clock package’s second hand). One prop:
look. Niche by design — reach for draw before inventing gauge uses.
list
A repeater over an array — the full mechanics live in the display system.
| Prop | Meaning |
|---|---|
bind / item / max | the array / per-element tree / cap (default 16) |
dir / gap / justify / align | layout (row or col) |
key | item field enabling keyed in-place updates |
overlap | { item_w, max_gap? } — spread-then-overlap facepiles with island separator rings |
{ type = "list", bind = "faces", key = "name",
overlap = { item_w = 35, max_gap = 10 }, justify = "center", item = face() }
button
A clickable that fires its action string to your on_command. Content:
icon (Phosphor name) and/or text, or custom glyph children (a
shape).
{ type = "button", action = "startPause", w = 30, h = 30, bg = "recessed",
children = { { type = "shape", vb = {24,24},
paths = { { d = "M8 5v14l11-7z", fill = "current" } } } } }
Any node can carry action directly — a whole card can be clickable
without a button dressing.
field
An editable input. multiline (textarea vs input), placeholder, max
(length). bind seeds it; edits return through on_widget_state
(debounced 800 ms, flushed on blur). A data push never clobbers a focused
edit.
{ type = "field", bind = "text", multiline = true, placeholder = "Notes…" }
draw
A metered immediate-mode canvas for the long tail no primitive covers —
gauges, graphs, dials. bind = the trigger field; on_draw(host, d, w, h)
= a Lua painter recording ops on host.g. Redraw is
data-driven only — the painter runs when the bound field changes, never
per frame, so idle cost stays structurally zero.
{ type = "draw", w = 64, h = 64, bind = "reading", on_draw = logic.draw_gauge }
When you want to build a thing, read the builtin that already does it — the recipe map points at the best worked example of each mechanic.