Skip to content

Theme builder

Pick one brand color and this builder derives the rest of the data-color accent set — the hover shade, the soft tint, and the text-on-primary color — checks the WCAG contrast for you, and generates ready-to-use exports.

Two modes:

  • Accent — adds a new [data-color="<name>"] axis (opt-in, sits alongside the built-in five). Just the accent changes.
  • Full theme — customises the default look: accent plus a neutral ramp (gray / slate / zinc / neutral / stone, driving surface / text / border / secondary), the control radius, and typography (body / heading / mono font-family), across both light and dark. The output is computed as the diff of the themed build against the stock build, so it’s exactly the variables that changed — nothing more.

It works by running the real token transformer (@hypermedia-components/core/token-transform) on the real DTCG sources right in your browser. The preview, the Apply to this page button, and every export are produced by the same buildTokensCss the library build uses — so a custom theme behaves exactly like a built-in one (every affected component variable, subtree-safe), and the “DTCG sources → generated CSS variables” model is never bypassed.

In OKLCH
ActiveNeutral
Saved

Semantic status colors (success / warning / error) stay fixed — the accent doesn't touch them.

Heading sample

Body text on this ramp — press ⌘K (mono). Select me for ::selection.

Scoped to this box and rendered by the real token build. Toggle the site's light/dark switch to check both. Apply to this page themes the whole site.

Additive — keep the stock CSS and load this after it. No build.

Everything is derived in OKLCH, using the same ladder the library’s own ramps are generated from — so a theme built here is put together exactly the way the five built-in accents are.

OutputDerivation
action-primary-bg / borderYour chosen brand color, as-is, re-expressed as oklch().
action-primary-hover-bg / borderThe brand color stepped down in lightness by the Hover darkness amount, holding hue and chroma. The default 7 is the gap the built-in ramps use between 600 and 700.
action-primary-fgAuto applies the lightness threshold: white at or below 0.55, otherwise gray.900. Override it manually if you prefer.
action-primary-soft-bgcolor-mix(in oklab, <brand> N%, transparent) — one value that blends correctly on light and dark surfaces.
focus-ringDefaults to the brand color.

The old builder multiplied sRGB channels to darken and ran a contrast search to pick the foreground. Both are gone. Darkening in sRGB dims warm hues faster than cool ones, so the same “12%” produced a different perceived step per brand; a lightness delta does not. And the foreground no longer needs a search, because at a fixed OKLCH lightness the contrast ratio barely moves with hue or chroma — the crossover is a constant, so it is a comparison.

The In OKLCH read-out shows where your brand color sits, and how far it is from step 600 of the ladder for its hue. Snap to ladder replaces it with that exact step — the color a built-in axis would use for the same hue, AA-safe on white text by construction.

Snapping is optional. Brand fidelity usually wins, and any color that clears the contrast read-out is fine. It is there for when you are picking a color rather than matching one — and Shuffle now rolls a random hue through the ladder, so a shuffled theme always clears AA.

The contrast read-out is the WCAG 2.x ratio between the brand color and the resolved text color. Aim for 4.5 : 1 (AA) or better; the badge turns red below that. If it fails, the brand color’s lightness is in the band around 0.62 where neither white nor dark text clears AA — snapping, or moving the lightness away from that band, fixes it.

The builder runs buildTokensCss with your color injected as a synthetic color.<name> source, then hands you the output three ways:

  1. Theme CSS block — just this theme’s [data-color="<name>"] block (every affected component variable, resolved). Additive: keep the stock token CSS and load this after it, then set data-color="<name>". No build. This is the correct Path A — overriding the seven semantic variables by hand would not work (see How the cascade actually works).

  2. DTCG token filecolor.<name>.tokens.json. Save it under packages/core/src/tokens/ and register it in build-tokens.mjs to ship the theme in the build (Path B). Also feeds external DTCG tooling (Style Dictionary, Figma).

  3. Full token CSS — the complete hc.tokens.css (every built-in theme plus yours). A drop-in replacement for the stock token stylesheet when you’d rather swap one file than add a block.

A fourth tab, Email templates, bakes the same resolved tokens into table/inline-style HTML email fragments (Thymeleaf or plain-HTML flavor) with a live preview — see the HTML email guide.

Apply to this page injects the generated block onto the docs site’s <html> so you can browse every component under your theme; Reset clears it.