← All posts

Rux v0.4.0: the CSS you actually write, and Rux in a browser

Pseudo-classes, custom properties and @media close the three gaps that made real stylesheets impossible. A dev overlay means a broken file says so instead of opening blank. And the whole runtime now compiles to WebAssembly, so you can try Rux without installing it.

v0.3 made updates cheap. v0.4 is about the two things that were stopping anyone else from using this: the CSS you would actually write did not work, and when something was wrong Rux said nothing. Both are fixed. On top of that the runtime now compiles to WebAssembly, so there is a playground you can open instead of a repository you have to clone.

What landed

Pseudo-classes. :hover, :focus, :active and :checked all match. They stack, they carry class-level specificity, and they work anywhere in a chain, so .card:hover .title recolours the title while the pointer is over the card. The interesting part is what it costs: a node that any pointer-state rule could match is tagged with its tree path, and the layout emits a region only for those. A document with no :hover rules emits none and pays nothing. When the pointer moves, only the subtree where the old and new chains diverge is re-cascaded, reusing v0.3's node splice, so a caret or a scroll position elsewhere survives by node identity.

Custom properties and var(). --name declarations inherit like color, and substitution happens after the cascade and inline styles have merged. That ordering is the whole trick: every property supports variables without its parser knowing they exist, including in style= and :style. Fallbacks, variables defined from other variables, and per-subtree overrides all work. examples/theme.rux swaps an entire palette with one :class.

@media queries. Rules inside a block that does not match are never emitted, so matching, cascade and specificity are untouched. A resize that crosses no breakpoint re-cascades nothing, and a document without @media never re-cascades at all. One landmine worth writing down: lightningcss normalises (min-width: 600px) into the Level 4 range form (width >= 600px) before we see it, so the range spelling is the one that actually arrives and the min-/max- arm is the compatibility path, not the main one.

A dev overlay. A broken file no longer opens an empty window. The failure is painted above everything, with line and column offset onto the file's own numbering so it matches your editor's gutter. A failed hot-reload keeps the last good tree on screen and marks it stale, so a typo mid-edit neither blanks the window nor slips past. Warnings are collected rather than only printed: unhonored properties, unknown pseudo-classes, undefined variables, unsupported media conditions, and expressions that failed to compile. Every shipped example is now asserted to load warning-free, so a noisy overlay in examples/ is a test failure.

Accessibility. role= used to mean something to selectors and nothing to anyone else. Rux now publishes a real accessibility tree through accesskit: roles, names, values, checked state and on-screen bounds. Names come from label=, else a linked <text for="…">, else a placeholder; a button is named by the text inside it. The tree is republished per frame but only while assistive technology is actually attached, so it costs nothing the rest of the time. It was verified by querying the live UI Automation tree, which is what activates the adapter, so that is a genuine end-to-end check rather than a mock.

Rux runs in a browser. The whole runtime compiles to wasm32, and the playground runs it on a canvas via WebGPU. The thing worth saying is what it is not: it is not a re-implementation. The canvas drives the same shell the desktop window drives, so input, focus, the caret, selection and scrolling cannot drift from the real runtime, because there is only one of them. Getting there needed six changes, all cfg-gated: no filesystem, no blocking on the main thread, no OS clipboard, and no system fonts, which is the nastiest of them. A browser exposes no font source at all, so fontique finds nothing, every string measures to zero, and you get a blank canvas with no error anywhere. The bundle embeds a font for exactly that reason.

Alongside it: a five-chapter guide that builds a task list from an empty file, with every snippet checked by the test suite; the playground's syntax colouring, which runs the same TextMate grammar VS Code and this site use rather than a third copy; and a formatter that indents markup and pretty-prints CSS.

The bug I only found by looking

Thirty-seven test binaries were green. A finger could not tap anything.

Touch was implemented for dragging content and nothing else: it recorded a position, scrolled on move, and cleared on release. It never dispatched a tap. So on any touchscreen, every button, checkbox and text field was dead. The reference doc had carried "untested: no touch hardware here" for weeks, and I had read that line many times as a caveat rather than as a warning.

The playground is what found it. Rux went in front of a phone for the first time and the bug surfaced in under a minute, along with a canvas sized 420 pixels wide on a 360 pixel screen. Neither was reachable by any test I could run.

The lesson is a corollary of the one this project keeps relearning. The standing rule is that a feature is not done until it has been driven in the window. The corollary is that "there is no hardware to test it on" is a reason to distrust a path, not a reason to call it done. Anything marked untested should be read as probably broken.

There was a second one, in the same family. The pointer leaving the window fires CursorLeft, not a CursorMoved, so a hovered element stayed lit after the pointer was gone. Set but never cleared, which is the exact shape of v0.1's worst bug. It now has a test that asserts the clearing, not the setting.

What this release is not

The overlay cannot be dismissed, and CSS warnings have no line numbers yet, so it tells you what is wrong without always telling you where. There is no rux check, so none of those diagnostics are available to CI or an editor.

The accessibility tree is flat, and action requests from assistive technology are received but not dispatched. Screen readers can read a Rux window; they cannot yet drive it.

One hole is not fixable from outside the engine: rhai returns () for a missing map property, so {{ user.nmae }} still renders empty and silently. That is precisely the failure the overlay work exists to kill, and it is now the second recorded motivator for forking rhai, beside the older one that no function can mutate a signal.

The synthetic checked class still works alongside :checked for one more release, then goes.

The playground needs WebGPU, and it is a browser on a phone, not a native mobile app. Mobile has been in the README since the first commit and there is still not one line of mobile code in the repository. That gap is now scheduled rather than implied.

Unchanged from v0.3 and still the largest gap: there is no true inline text flow, so two <text> elements cannot share a line and bold inside a sentence is not expressible. A component takes props but cannot emit events or render children. r-for has no index and reconciles by count rather than by key. The reactive tier still has no effects or computed values.

Next

v0.5 is about being usable by someone who is not me: publishing to crates.io, rux fmt and rux check as real subcommands, and the playground catching up to the overlay this release just built. The roadmap now plans every version through 1.0.