Theme maker
Pick a brand colour, a neutral temperature and a corner language. Everything else —
two twelve-step ramps, the whole neutral chain, both schemes, and all eight
--color-content-on-* foregrounds — is derived and checked against WCAG AA
before you see it. The output is a recipe you can hand to
create-spoke.
Why a generator rather than a form
The interesting part is not the colour picker. It is that a hand-filled theme has
never once declared the eight --color-content-on-* foregrounds — the
spoke template offers no slots for them, and the contrast gate blocks on all eight.
That is why beacon fails
content-on-brand-muted at 3.64:1, and why the hub's own defaults
fail seven pairs today. Choosing a foreground that reads on the fill you just
generated is the job here; the swatches are incidental.
Your exact brand hex lands on step 9 of the generated ramp and is copied through untouched. The other eleven steps are interpolated in OKLCH along the lightness and chroma curve of the nearest Radix ramp, so they inherit that ramp's tuning without approximating your colour.
Seeds
Preview
Scoped to this container, not the page — the docs chrome around it stays put so you can still read while you edit. Custom properties inherit through shadow boundaries, so the web components below pick them up with no per-component wiring.
Survey 2026-03
Flows were within the modelled envelope for eleven of twelve reaches. The twelfth is flagged for review.
A sunken well, a field, and a link — the surfaces most themes get wrong.
Contrast
The same 29 pairs check-contrast.mjs gates on, resolved through the same
reader. A fail here is a fail in CI.
…
Derived values
Everything the seeds produced. Edit any value to pin it — pins are recorded in the recipe and applied last, so regenerating never clobbers them. Reset a pin to hand the value back to the generator.
Output
Save the recipe as theme-acme.json, then either
regenerate any time —
node scripts/make-theme.mjs --recipe theme-acme.json --out ../acme-design/src/styles — or scaffold a whole spoke from it:
node scripts/create-spoke.mjs --name Acme --slug acme --dir acme-design --theme theme-acme.json The generated CSS
…