Color
Engineered, not picked. Every color is stored as hex, converted to OKLCH at build time, with its lightness pinned to a perceptual grid. The five neutral bases share one grid — the same shade step is equally light in every base — while each chromatic hue follows its own lightness curve, centred where that hue is most vivid, and takes its chroma at the edge of the Display-P3 gamut. Contrast is then gated by APCA before it can ship.
The palettes
Start with the five neutral bases — eleven shades each. Read down any column: every base’s 500 sits at the same perceptual lightness, only hue and chroma differ. Below are the chromatic hues brand and status are built from — these each follow their own lightness curve, so a vivid yellow can stay light while red runs deep — then the full reference. One color-vision filter applies to them all.
Brand & status hues
Brand modes are blue, purple, pink, teal, orange, and black. The chromatic brand modes share the OKLCH pipeline; black is a monochrome semantic recipe built from black.base, white.base, and neutral support. Switch to a color-vision filter and watch the red/green pair (error/success) converge: that’s why status never relies on hue alone.
All color scales (17)
The full chromatic set. Brand uses blue, purple, pink, teal, and orange; status uses red, yellow, green, and blue. The rest are raw primitives available for data viz and one-off use.
Live tokens
Semantic surface/foreground pairs — the real tokens components use. Open the theme picker (bottom-right) and swap the base or dark mode: every pair re-resolves live, and each clears the APCA gate by construction.
Foreground describes its relationship to a surface; text describes one application. The relationship comes first when naming a token. Text and icons share that color; a foreground can also paint a control part or indicator on the same surface. For example, primary-foreground colors both a button’s label and a checked switch’s knob on primary-background. Likewise, error-subtle-foreground colors the message and icon on error-subtle.
A foreground has a default background partner, not necessarily an exclusive one. Use it on other surfaces only with contrast coverage for the intended content tier. A token name alone does not guarantee readability on every background or at every text size.
What each shade is for
The shade number is a luminance coordinate, and each step maps to specific semantic roles. The mapping is not a simple light/dark flip — a shade lands at a different step in dark mode to hold the same perceptual tier. Text and hairline borders are the exception: foreground, muted-foreground and border.default are alpha tokens — transparent black in light, white in dark — so they composite onto any surface instead of binding to a neutral shade.
| Shade | Role | Light-mode use | Dark-mode use |
|---|---|---|---|
| 50 | Near-white | muted, disabled, background/container/popover-hover | nav-foreground |
| 100 | Very light | background-active, container-active, control-background, nav-background | — |
| 200 | Light | control-background-hover, nav-item-hover/active, nav-border | — |
| 300 | Light-medium | — | disabled-foreground, nav-muted-foreground |
| 400 | Medium-light | border-focus, disabled-foreground | border-focus |
| 500 | Mid | — (perceptual anchor) | — (anchor) |
| 600 | Medium-dark | nav-muted-foreground, brand -background | — |
| 700 | Dark | brand -background-hover | control-background-hover, popover-hover |
| 800 | Very dark | brand -background-active | control-background, popover, container-hover, nav-border |
| 900 | Darker | container-foreground, popover-foreground, nav-foreground | container, muted, background-hover/active, nav-item-hover/active |
| 950 | Near-black | — (rarely surfaced) | background — the canvas, nav-background, disabled |
Accessibility
Contrast is gated by APCA (not WCAG 2 ratios), with thresholds set per intended use. A failing pair blocks the build — thresholds are not negotiable per finding. The palette is also validated against Viénot-simulated dichromacy in CI; the toggle above is the visual preview of that test.
| Pair | Min APCA | Covers |
|---|---|---|
| foreground ↔ background | Lc ≥ 75 | Body text |
| {brand,status}-foreground ↔ -background | Lc ≥ 60 | UI labels — buttons, badges |
| *-subtle-foreground ↔ -subtle | Lc ≥ 60 | Labels on tinted fills |
| nav-foreground ↔ nav surfaces | Lc ≥ 60 | Nav label text |
| chart.categorical ↔ container | Lc ≥ 60 | Chart marks |
| muted-foreground ↔ muted | Lc ≥ 45 | Incidental / de-emphasised text |
| disabled-foreground ↔ disabled | Lc ≥ 45 | Disabled-state text |
| focus ring ↔ every surface | Lc ≥ 45 | Focus indicators |
How it works
- 1Store hex
Tokens live as hex on disk — the only format Figma and Tokens Studio round-trip cleanly.
- 2Convert to OKLCH
At build time every hex value is converted to an oklch(…) value emitted into CSS.
- 3Pin the lightness
Each shade’s L is overwritten by a perceptual grid — the neutral bases share one ladder, while each chromatic hue follows its own curve (yellow peaks light, red deep). Neutrals keep their source chroma; the re-pitched hues take it at the Display-P3 cusp.
- 4Gate with APCA
Every foreground/background pair is scored with APCA before merge. A failing pair blocks the build.