GitHub

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

Lowercase; becomes [data-theme="…"] and the file name.

Type a hex, or start from one of the 24 Radix hues — tier 1 ships all of them, and a real step 9 seeds the ramp it came from rather than an approximation of it.

The exact hex lands on step 9.

Neutral temperature
Corner language
Fonts and the non-brand intentions

Leave any of these empty to keep the hub default. Status colours are deliberately not derived from your brand hue — a green brand must not make danger green.

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.

Reach assessment Sacramento–San Joaquin Delta
Primary Secondary Info Success Warning Danger

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
…