Skip to content

UI Components

This page documents the GPU renderer’s element vocabulary (electrobun/main/ui). The DOM renderer (electrobun/browser/ui) shares the same reactivity and control flow but speaks HTML — any tag works there.

In JSX, elements are the intrinsics <box>, <row>, <column>, <text>, and <spacer>; everything else here (each, dynamic, textInput, webview, wgpuSurface) is a builder function used inside a JSX builder-escape child: {() => ui.each(...)}. The same functions are also a complete JSX-free API for plain .ts files — see What the JSX becomes.

import { live, ui, textInput, webview, wgpuSurface, onKey } from "electrobun/main/ui";

Any prop typed Reactive<T> accepts a plain value or a live() scope: bg={live(() => hover() ? "#232336" : "#1b1b28")} re-runs only when its dependencies change and updates only that property. Bare functions in value positions throw — so live( is a complete, greppable index of a component’s reactivity. Event handlers and builder-escape children stay plain functions. Control-flow components (<Show>, <For>, <Switch>/<Match>) follow the same rule: a live() prop reconciles; a plain value is a frozen snapshot.

ui.box(props, children?)

The universal building block: a flex container, an optionally painted rectangle, and an input target.

Layout props

PropTypeDefaultPurpose
dir"row" | "column""row"Main axis.
gapReactive<number>0Space between children.
padReactive<number>0Uniform padding.
width, heightReactive<number>autoFixed size; auto sizes to content.
growReactive<number>0Share of leftover main-axis space.
justify"start" | "center" | "end" | "between""start"Main-axis distribution.
align"start" | "center" | "end" | "stretch""stretch"Cross-axis placement. Stretch fills auto-sized, non-text children (flexbox rule).
overflow"visible" | "scroll""visible""scroll" clips children and offsets them by scroll on the main axis.
scrollReactive<number>0Scroll offset for overflow: "scroll" boxes.

Paint props

PropTypePurpose
bgReactive<string | number>Background color; alpha 0 paints nothing.
radiusReactive<number>Corner radius (SDF, antialiased).
borderReactive<number>Border width.
borderColorReactive<string | number>Border color.

Interaction props

PropTypePurpose
onClick, onPointerDown, onPointerUp, onPointerEnter, onPointerLeave(e: PointerEventInfo) => voidPointer handlers; any handler makes the node hittable.
onKeyDown(e: KeyEventInfo) => boolean | voidReceives keys while this node (or a descendant) is focused; return true to stop bubbling.
focusablebooleanClick-to-focus target; participates in focused key routing.
windowDragbooleanDragging this node moves the host window; a still click delivers onClick.

ui.row / ui.column

ui.box with dir preset. ui.column({ pad: 16 }, () => { ... }).

ui.text(content, props?)

Single-line text. content is Reactive<string | number>.

PropTypeDefaultPurpose
sizeReactive<number>14Glyph height in points.
colorReactive<string | number>whiteText color.

Text measures its intrinsic size; there is no wrapping or truncation yet, so size strings to their containers.

ui.spacer(grow?)

Flexible (default grow: 1) or weighted empty space on the main axis. ui.row({}, () => { left(); ui.spacer(); right(); }) right-aligns right().

ui.dynamic(props, builder)

A reactive region: builder re-runs — and the subtree rebuilds — whenever a signal it reads changes. Props are ui.box props. Use it for conditionals and small lists; prefer ui.each for keyed lists.

<column>
{() =>
ui.dynamic({}, () => {
if (!loggedIn()) ui.text("Sign in");
else ui.text("Welcome back");
})
}
</column>

ui.each(props, items, key, render)

Keyed list reconciliation. Rows are diffed by key: unchanged rows keep their subtree (and any per-row state) across filters and reorders; removed rows are disposed with their reactive scope.

<column grow={1}>
{() =>
ui.each(
{ dir: "column", gap: 4 },
() => state.todos, // Accessor<readonly T[]>
(todo) => todo.id, // stable key
(todo, index) => { // index is a reactive accessor
ui.row({ pad: 8, bg: live(() => (index() % 2 ? "#16161e" : "#1b1b28")) }, () => {
ui.text(todo.title);
});
},
)
}
</column>

ui.anchor(props)

A layout-only rectangle that paints nothing and reports its computed frame — the primitive that native layers build on.

PropTypePurpose
width, height, growReactive<number>Layout sizing.
onFrame(rect: { x, y, width, height }) => voidCalled whenever layout moves or resizes the anchor.

wgpuSurface(props)

A real Dawn WGPUView positioned by the layout — the UIWindow equivalent of the <electrobun-wgpu> tag. The view is created lazily on first layout and removed with its reactive scope.

PropTypePurpose
width, height, growReactive<number>Layout sizing.
transparentbooleanStart the native view transparent.
onReady(view: WGPUView) => voidCalled once, after first layout. Render into the view with the WebGPU adapter.
onFrame(view, rect) => voidCalled on every subsequent layout move/resize.

webview(props)

An out-of-process webview positioned by the layout — the UIWindow equivalent of the <electrobun-webview> tag. Created lazily on first layout, removed with its scope.

PropTypePurpose
urlstringRemote or views:// URL.
htmlstringInline HTML instead of url.
width, height, growReactive<number>Layout sizing.
partitionstringSession partition (see BrowserView).
sandboxbooleanSandboxed (events-only) webview.
onReady(view: BrowserView) => voidThe created BrowserView — use it for RPC, navigation, etc.

textInput(props)

A controlled single-line text input: focus ring, caret with blink, editing (insert, backspace, word/line navigation via alt/cmd), placeholder, submit.

const [query, setQuery] = createSignal("");
<column pad={10}>
{() =>
textInput({
value: query,
onInput: setQuery,
onSubmit: (value) => run(value),
placeholder: "Search...",
autofocus: true,
})
}
</column>
PropTypePurpose
value() => stringControlled value accessor.
onInput(next: string) => voidCalled with each edit.
onSubmit(value: string) => voidEnter.
placeholderstringShown while empty.
autofocusbooleanFocus on mount.
sizenumberFont size (default 14).
grow, width, pad, radius, bg, border, borderColorReactive<...>Container styling.
focusBorderColorReactive<string | number>Border while focused.
color, placeholderColor, caretColorstringText/caret colors.

Unhandled keys (arrows up/down, Escape) bubble to window-level onKey handlers — which is how list navigation under an input works.

Control flow: Show, For, Switch/Match

<Show when={live(() => state.threads.length > 0)} fallback={<text>empty</text>}>
<For each={live(() => state.threads)}>
{(thread) => <text size={12}>{thread.subject}</text>}
</For>
</Show>

Both forms are valid on every control-flow prop: when={live(...)} / each={live(...)} reconcile on change, while a plain value — <For each={state.threads}> — renders a frozen snapshot once and never updates. <For> accepts key={(item) => id} (defaults to item identity) and fallback; <Switch fallback={...}> picks the first <Match when={...}> whose condition is truthy.

Window-level keyboard: onKey(handler)

import { onKey, Key, Mod } from "electrobun/main/ui";
onKey((e) => {
if (e.keyCode === Key.Escape) close();
if (e.modifiers & Mod.Cmd && charForKey(e.keyCode, 0) === "c") copy();
});

Key (Return, Tab, Space, Backspace, Escape, arrows) and Mod (Shift, Ctrl, Alt, Cmd) are exported constants; charForKey(keyCode, modifiers) maps key codes to characters (US layout), and applyEditKey is the pure text-editing reducer textInput uses — both usable for custom widgets.