Skip to content

Warren in the Browser (DOM)

electrobun/browser/ui is Warren’s DOM renderer. It shares the exact same reactivity core and JSX semantics as electrobun/main/ui — components run once, reactivity is explicit via live(), control flow is For/Show/Switch/Match — but renders into real DOM nodes inside a webview (or any web page). One mental model across your whole app: GPU chrome in the main process, DOM documents in webviews, the same signals and stores in both.

There is still no compiler: Electrobun’s view bundler transpiles .tsx natively. Point TypeScript at the DOM runtime (type-checking configuration only):

// tsconfig.json for your view code
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "electrobun/browser/ui" } }

If the same project also contains main-process Warren code, use a per-file pragma instead of the global setting:

/** @jsxImportSource electrobun/browser/ui */

Mounting

import { live, signal, render } from "electrobun/browser/ui";
function Counter() {
const [count, setCount] = signal(0);
return (
<div class="counter">
<output>{live(() => `Count: ${count()}`)}</output>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
</div>
);
}
const dispose = render(() => <Counter />, document.getElementById("app")!);

render(app, container) mounts the app and returns a dispose function that removes everything Warren created (the container’s other content is left alone).

Elements and props

Any HTML or SVG tag works — the runtime never enumerates elements; a tag becomes document.createElement(tag) (createElementNS inside <svg>). Prop handling:

PropBehavior
class / classNamesets className (attribute on SVG)
classListobject of class names to booleans; toggles each
stylestring (cssText) or object (camelCase keys, --vars supported)
value, checked, innerHTML, …set as element properties
disabled, other booleansboolean attributes (true sets, false/null removes)
onClick, onInput, on...addEventListener(name.slice(2).toLowerCase())
refref={(el) => ...} called with the created element
anything elsesetAttribute (name passed through — SVG’s viewBox works)

Reactivity is explicit, exactly like the GPU renderer: class={live(() => ...)} updates in place; class={value} is static; a bare function in a value position is an error.

<div
classList={live(() => ({ active: isActive(), busy: pending() }))}
style={{ marginTop: "8px", "--accent": "#b18aff" }}
/>

Text inputs are explicit two-way:

<input value={live(name)} onInput={(e) => setName(e.target.value)} />

Control flow

Show, For, Switch/Match behave identically to the GPU renderer: a live() prop reconciles on change, a plain value is a frozen snapshot. For reconciles keyed rows against real DOM nodes — reordering moves the same elements (scroll position, focus, and animation state survive):

<For each={live(() => state.rows.slice())} key={(row) => row.id}
fallback={<p>No rows.</p>}>
{(row, index) => <li classList={live(() => ({ first: index() === 0 }))}>{row.label}</li>}
</For>

Regions are delimited by comment nodes, so conditional and keyed content works inside any parent (<tbody>, flex rows) without wrapper elements.

Portal

Portal renders children into another parent — document.body by default, or mount — while keeping reactive ownership, so they’re torn down with the owning scope:

<Show when={live(modalOpen)}>
<Portal>
<div class="modal">...</div>
</Portal>
</Show>

Escape hatch

A bare function child runs imperatively at that point in the tree; currentParent() returns the element it would attach into, and a returned element mounts. Use it to integrate DOM libraries (editors, terminals) that manage their own subtree:

import { cleanup, currentParent } from "electrobun/browser/ui";
<div class="terminal-host">
{() => {
const host = currentParent() as HTMLElement;
const term = attachTerminal(host); // any third-party DOM library
cleanup(() => term.dispose());
}}
</div>

Sharing code with the main process

The reactivity primitives (signal, store, live, memo, inert, cleanup, batch) are the same implementation in both entries — model and store modules written against them run unchanged in the main process and in views. Only the element vocabulary differs: box/row/column/text on the GPU renderer, HTML on the DOM renderer.

See Warren reactivity for the full semantics — everything there applies verbatim in the browser.