Elements, input and events
What HTML renders to on the grid, how form controls behave, and how events reach your handlers. Anything unlisted renders as a plain box per its display default.
Text and structure
Headings, paragraphs, lists (ul/ol with markers), blockquote, pre, hr (a ─ rule), figure, dl, and the text-level elements
(strong/b, em/i, u, s/del, mark, code, kbd, abbr, samp, var) carry a browser-like UA stylesheet in cells. img renders (see Images below); video, canvas, and iframe are not rendered.
Images
<img src="..."> renders as half-blocks — each cell shows two vertically
stacked pixels (’▀’ with per-half colours), so a 40×20-pixel image is
40 columns × 10 rows. Sources: file paths and data:image/png;base64 URIs (PNG: 8-bit RGB/RGBA/greyscale/palette, decoded with no native
dependencies). Sizing follows CSS width/height in cells, defaulting
to the intrinsic pixel size; loading is async and reflows on arrival.
Mostly-transparent pixels show the terminal background.
On terminals that speak the kitty graphics protocol (kitty, Ghostty, WezTerm — detected automatically), the image renders as real pixels scaled to its cell box, covering the half-blocks. Everywhere else the half-blocks are what you see.
Links
<a> is underlined and focusable. Enter or a click fires click; if
unprevented, the href opens in the local browser.
Form controls
| Control | Rendering | Interaction |
|---|---|---|
input (text) | one-row editor on the grid | readline-style editing with a real cursor; input events carry { value, cursor }; maxlength caps typing and paste; readonly blocks edits but keeps caret movement |
input type="password" | as text, value painted as • bullets | editing as text inputs; the real value stays in value |
textarea | multi-line editor | as text inputs, including maxlength/readonly |
input type="checkbox" | [x] / [ ] (3×1) | Space or click toggles; change/input carry { checked, value } |
input type="radio" | (•) / ( ) | selecting unchecks same-name radios across the tree; never untoggles itself |
select + option/optgroup | selected label + ▾, sized to the longest option | popup-less cycling: ArrowUp/ArrowDown move with wraparound, Space/Enter/click advance; change carries { value } |
button | centred text, stylable | Enter/click dispatch click |
progress / meter | 20×1 block bar: █ fill, eighth-block partials, ░ track | value/max (+min for meter); no value = indeterminate track |
details / summary | ▶/▼ disclosure marker | Enter/click toggles open; fires toggle with { open }; closed details hide non-summary children |
label | plain inline text | clicking activates its control — wrapping or for="id" both work |
State attributes drive selectors: :checked, :disabled/:enabled, [open]. disabled controls are skipped by focus traversal and swallow
clicks. DOM-compat properties exist where Svelte bindings expect them: el.checked, el.value.
Focus
Tab / Shift+Tab cycle button, input, textarea, a, select, summary in document order, skipping disabled controls. Clicking a
focusable element focuses it. The focused element matches :focus —
style it; there is no default focus ring beyond your CSS.
button:focus { border-color: yellow; } Keyboard
Enter— activate the focused element (click / toggle / cycle).Space— toggle checkbox/radio, advance select.- Arrows — cycle selects; move the text cursor in inputs.
- Printable keys — type into the focused input/textarea.
- Unhandled keys dispatch
keydownto the focused element (or the tree root) with{ key, ctrl, shift, meta }. Ctrl+Cexits (add'ctrl+d'torun’sexitOnfor EOF-style exit).Ctrl+Ztruly suspends: the terminal is restored for the shell, andfgre-enters modes and repaints with state intact.- The kitty keyboard protocol is enabled where supported, so combinations the legacy encoding can’t
express (
Ctrl+Enter,Shift+Space, …) arrive with correct modifiers. Elsewhere the classic encoding applies.
Modal dialogs
A <dialog open> captures input like a browser modal: Tab/Shift+Tab trap inside it, focus is pulled in from outside, and Escape removes open and dispatches a close event. Style it with position: absolute + z-index to float above the page.
{#if confirming}
<dialog open onclose={() => confirming = false}>
<span>Delete everything?</span>
<button onclick={confirm}>Yes</button>
<button onclick={() => confirming = false}>No</button>
</dialog>
{/if} Mouse
Enabled by default: click (focus + click + default action), wheel
scrolling of overflow: auto|scroll boxes (with scrollbar overlays),
hover driving :hover. Mouse events carry cell coordinates.
The event model
Events dispatch with W3C capture/bubble semantics through the component
tree; stopPropagation() and preventDefault() work, and default
actions (link opening, details toggling, select cycling, label
activation) respect preventDefault().
Payloads ride on event.data — { value, cursor } for text input, { checked, value } for checkables, { open } for toggle, { cols, rows } for region resize. Handlers shared with the browser
should read both shapes:
<select onchange={(e) => plan = e.data?.value ?? e.target.value}>