astryx-svelte v0.3.0: porting a design system you can check

A Svelte 5 port of Astryx at 1:1 with upstream 0.3.0 — and the two oracles that prove it, including what they cannot see.
Rohit Kushwaha
Aug 10, 2026
5 min read
Porting a design system is mostly a question you cannot answer by looking: is this the same thing? Not "does it work" — does a Button here render what a Button there renders, down to the declaration.
You can review a port. Reviewing scales badly, and it scales worst exactly where the port is most faithful, because faithful code is boring to read and a single wrong --spacing-2 looks like every other correct line on the page.
So this port is built around a different answer: compile both and diff the output.

The property that makes it work

Astryx styles components with StyleX, which compiles stylex.create calls into atomic CSS classes at build time. The class names are a hash of the declaration — not of the file, the component, or the framework.
That has a consequence worth sitting with. If our .stylex.ts module declares the same properties against the same token references upstream's .tsx module does, the compiler emits byte-identical classes. Not similar. Identical. Which means the question "is this the same thing?" has a mechanical answer, and the answer is a string comparison.
packages/core/scripts/compare-upstream-classes.mjs runs our modules through the StyleX Babel plugin and diffs the emitted classes against the already-compiled ones in the published @astryxdesign/core tarball. Today:
1528 style keys checked (19 of them as marker-normalised CSS),
615 inline call sites checked, 0 skipped, 0 mismatches
The theme packages get the same treatment from packages/themes/neutral/scripts/compare-upstream.mjs, which diffs generated theme CSS declaration by declaration: 2,418 declarations across seven packages, 0 mismatches.
That oracle is bidirectional, which matters more than the headline. A missing declaration fails. A wrong value fails. An invented one fails too — that is the direction most parity checks forget, and it is the one that catches a port quietly becoming a fork.

Deferrals that expire

Real ports need to defer things. The trouble with a deferral is that it outlives its reason and nobody notices.
Every skip in these oracles is an explicit entry with a written reason, and the list is checked in both directions: a skip that stops matching fails the run, and so does a skip whose key starts matching. You cannot leave a stale exception lying around, because the stale exception is itself a failure.
The skip list is currently empty. Every deferral the port ever wrote retired itself, and each one retired by failing a build and demanding to be deleted.

What the oracle cannot see

Here is the part a release post usually skips.
A stylex.create value can be a plain object or an arrow function. Arrow-function values carry no $$css marker for the extractor to find, so the oracle checks every static style and no function style at all — 54 of them across 32 modules.
We know the exact size of that blind spot because we measured it rather than assumed it. Inverting a !isDisabled guard in text-input's status-hover branch left the oracle at 0 mismatches, exit 0 — while the bug was live in 13 call sites.
A verification tool that has never been shown to fail is a tool nobody has tested. That number is in TODO.md under known debts, with the reproduction, because the useful thing about a blind spot is knowing where it is.

The failure mode the oracles do not cover at all

Class parity is a claim about CSS. It says nothing about whether a React idiom survived translation into Svelte 5 — a context storing a value where it should store a getter, a $derived cached across a server render, an un-untracked attachment. Those are the bugs that compile, pass the oracle, and break at runtime.
And there is a third category the tooling reaches even less far into, which this port ran headfirst into while writing this post.
The docs site had been rendering every live component example in the site's own brand theme instead of the neutral theme a reader installs. Upstream re-themes each preview through a small ComponentPreviewTheme boundary at six separate surfaces. This port had dropped all six — and had written down a reason: "a second identical boundary would be a no-op."
Three files said it. A fourth asserted the premise the others leaned on. The premise was false: the ambient theme is the brand theme, so the boundary switches the theme rather than repeating it.
Four files agreed with each other and none of them agreed with upstream. No oracle covers prose, and the comments were confident, specific, and wrong. A comment asserting parity is not evidence of it. Finding it took re-reading the reference tree, which is now a check the docs build runs on every commit.

What is in it

  • 101 / 101 upstream component directories at 0.3.0, with a bidirectional diff confirming nothing here is invented
  • 184 / 184 design tokens, and a 250-key en catalog byte-identical to upstream's
  • 8 theme packages — upstream's seven, plus one liquid-glass that ports nothing and is labelled as the port's own addition rather than smuggled in as parity
  • 626 example blocks on the docs site, transcribed from upstream's own blocks rather than re-authored, each naming its source
  • Upstream's 42 page templates, ported as real SvelteKit routes

What is not

The CLI is a placeholder and is marked private — it is not in this release. lab, charts, vega and richtext have not been started. And the full browser test suite does not yet complete in one run: 4,760 tests pass with zero failures, but the shared Chromium instance dies late in the run and takes the remaining files with it. That is infrastructure rather than product, and it is the next thing to fix.
Upstream bugs are reproduced, not corrected, and each is written down. A port that fixes things quietly is a fork with extra steps.

Try it

The source is on GitHub. Every component page on this site renders live Svelte, and every example is a real file in the repository — including the one that was rendering in the wrong theme until it was not.
Svelte Parity Testing

Related

Getting started
Components
Themes