web-dom is an optional Tier 3 experimental target. Day compiles the Rust app to WebAssembly and connects controls to DOM elements. Browser permissions and capabilities differ from the native desktop and mobile targets; evaluate this target separately.

Getting started

The only prerequisite beyond the CLI is the wasm target.

rustup target add wasm32-unknown-unknown
cargo install --git https://github.com/daybrite/day day-cli
day new app my-app --toolkit web-dom
cd my-app
day launch -t web-dom     # builds, serves over loopback, opens your browser

day build -t web-dom writes a self-contained dist/: index.html, the shim, the stylesheet, your .wasm, and any bundled images and fonts. There is no day pack for web, because dist/ is already the deployable artifact: copy it to any static host.

day build -t web-dom --profile release
# dist/ → your bucket, your CDN, GitHub Pages, anywhere

Serve it over HTTPS (or localhost). Browsers won’t instantiate WebAssembly from file:, and sensors require a secure context.

Running dayscripts

A dayscript runs against the web build the way it does on every other target, with one addition: a page cannot screenshot itself, so the runner captures through a headless browser that day drives. Day bundles the driver (day web driver prints its path); it needs Playwright and a browser for it, installed once into any directory:

mkdir -p ~/.cache/day-webdriver && cd ~/.cache/day-webdriver
npm init -y && npm install playwright
npx playwright install chromium

Then point day at it and run the script:

export DAY_WEB_DRIVER="node $(day web driver)"
export DAY_WEB_DRIVER_PLAYWRIGHT=~/.cache/day-webdriver
export DAY_WEB_DRIVER_BROWSER=chromium   # webkit (the default) or firefox also work
day launch -t web-dom --script dayscript/walkthrough.yaml

On Linux use Chromium: Playwright’s Linux WebKit has no Origin Private File System, which day-part-fs needs. Several --script flags run in one launch, against one app instance, which is how state from a seed script reaches the scripts after it; the browser profile, and the origin’s storage with it, lasts exactly as long as the launch.

Caveats

This backend covers a subset of what the native backends do. The following are missing or reduced:

  • External-piece support varies. The current coverage matrix includes DOM arms for date/time, color-picker, and text-editor pieces, while several other in-tree pieces have none. Enable the matching feature and check source coverage. External media/WebView/Lottie repositories were not inspected in this review.
  • The backend reports Cap::FileDialogs as native and Toolbar, AppMenu, and Tree as emulated. Browser user activation and permissions can still limit an operation; a capability is not a runtime acceptance result. Secondary windows use the cover path.
  • The list is emulated: one element per row, created eagerly, with no recycling, so long lists cost memory and time.
  • Split navigation is decided once at launch from the viewport width (≥ 700px), and does not re-evaluate on resize.
  • Accessibility is thinner here than on native. Semantics come from the tags Day uses (<button>, <input>, <select>, <progress>, <textarea>, <dialog>), and aria-label flows through from .a11y(...). But where a piece realizes as a <div> (label, tabs, navigation, the nav menu, segmented and radio pickers, the spinner), there is no compensating ARIA role. If you are shipping to an accessibility requirement, use a native target or budget for tweaks.
  • Parts have separate browser implementations and support queries. Do not infer availability from the DOM backend alone; for example, clipboard has asynchronous browser operations while synchronous helpers cannot wait for browser permission. Consult each part’s reference.
  • Piece registration differs. linkme’s distributed slices don’t compile for wasm, so web-dom keeps a runtime registry and a piece registers itself from its constructor. A piece needs an explicit dom feature to appear at all; see extending.

What each piece becomes

The generated coverage matrix, checked in CI, covers every backend; this table adds links and notes.

Day pieceHTML
column / row / nav_stack / section<div>
label<div> (not a <label>)
button<button type="button">
toggle<input type="checkbox"> styled as a switch
slider<input type="range">
text_field<input type="text">
secure_field<input type="password">
text_area<textarea>
picker().menu()<select> with <option>s
picker().segmented() / .inline()a <div> of <button>s
progress(f)<progress max="1">
spinner()a CSS-animated <div>
divider<div>
scroll<div> with overflow:auto
listscrolling <div> of absolutely positioned cells
nav / nav_menu / tabs<div>s (with <button>s for the tab strip and back control)
image<img>
vector<img> loading the staged SVG: day-dom requests .svg for names in the page’s vector list, and the browser renders it at display size; the raster PNG sits beside it as the older-host fallback
canvas<canvas>, display list replayed on a 2-D context
dialogs / alerts / prompts<dialog> via showModal()
external media()<video>

Text measurement uses a hidden offscreen element with the same computed font and a max-width, measured with getBoundingClientRect(), which gives the browser’s wrapping metrics, cached per element and width.

Typography is rem-based: Font::Body is 1rem, the other styles follow the Apple text-style ratios, and custom point sizes map to pt/16 rem, so if you raise your browser’s default font size, every Day font scales with it (canvas drawing text excepted, since it lives in the app’s coordinate space).

Pages report their own size back through a ResizeObserver, which is how CSS-managed panes stay in sync with Day’s layout.