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.

<a> is underlined and focusable. Enter or a click fires click; if unprevented, the href opens in the local browser.

Form controls

ControlRenderingInteraction
input (text)one-row editor on the gridreadline-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 bulletsediting as text inputs; the real value stays in value
textareamulti-line editoras 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/optgroupselected label + , sized to the longest optionpopup-less cycling: ArrowUp/ArrowDown move with wraparound, Space/Enter/click advance; change carries { value }
buttoncentred text, stylableEnter/click dispatch click
progress / meter20×1 block bar: fill, eighth-block partials, trackvalue/max (+min for meter); no value = indeterminate track
details / summary▶/▼ disclosure markerEnter/click toggles open; fires toggle with { open }; closed details hide non-summary children
labelplain inline textclicking 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 keydown to the focused element (or the tree root) with { key, ctrl, shift, meta }.
  • Ctrl+C exits (add 'ctrl+d' to run’s exitOn for EOF-style exit). Ctrl+Z truly suspends: the terminal is restored for the shell, and fg re-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.

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}>