Web (DOM)
Tier 3 · Experimentalweb-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::FileDialogsas native andToolbar,AppMenu, andTreeas 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>), andaria-labelflows 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 explicitdomfeature 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 piece | HTML |
|---|---|
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 |
list | scrolling <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.