Tokens
elements man style/tokens Read as markdownEvery visual decision in the system is a CSS custom property, a token. App CSS consumes tokens; it does not hardcode values. This is what makes rebranding, dark mode, and density adjustments one-line changes.
The three-tier model
Tokens are layered in three tiers, each building on the one below.
Tier 1: the scale (primitives). tokens/scale.css defines the raw,
non-color scales: the type ramp, weights, leading and tracking, the spacing
scale, radius, shadows, motion, z-index layering, breakpoint reference values,
and container widths. These are the vocabulary: --space-4, --text-lg,
--radius-md. They don't change between themes. See scale.
Tier 2: the semantic theme. themes/default.css defines the color ramps
(one neutral + four status ramps in OKLCH) and then maps them onto semantic
tokens: --bg, --ink, --rule, --accent, the status triples, focus rings,
and the per-component slots. It also flips those semantic tokens for dark mode.
This is the tier your app reads from. See color and
components.
Tier 3: the optional brand theme. A theme file imported after the default
repoints the accent block (and optionally a brand ramp) to color the whole
system. themes/elements.css is the shipped example;
themes/customer-example.css is the documented recipe. See
theming.
scale.css primitives --space-4, --text-lg, --radius-md
↓
themes/default semantic tokens --ink → --neutral-900, --accent → --neutral-900
↓
brand theme repoint accent --accent → your brand ramp (optional)
Consume semantic tokens, not raw ramps
The neutral and status ramps (--neutral-500, --danger-600, …) exist so the
semantic tokens have something to point at. In app CSS, reach for the
semantic token, not the ramp step:
/* Do this: follows the theme and dark mode automatically */
.notice {
color: var(--ink-soft);
background: var(--bg-soft);
border: 1px solid var(--rule);
}
/* Not this: a raw ramp step won't flip in dark mode */
.notice {
color: var(--neutral-700);
background: var(--neutral-50);
}
--ink-soft is --neutral-700 in light mode and --neutral-300 in dark mode.
Point at the semantic name and both come free. Reach past it to the ramp and you
opt out of theming.
Where the tiers live
| Tier | File | Layer |
|---|---|---|
| Scale primitives | tokens/scale.css |
tokens |
| Semantic theme + colors + dark mode | themes/default.css |
tokens |
| Brand theme (opt-in) | themes/elements.css |
tokens |
All three sit in @layer tokens, low in the cascade, so your app's own token
overrides, placed later, always win. See overriding.
- color: the neutral and status ramps and every semantic color token.
- scale: type, spacing, radius, shadow, motion, and the rest of the primitives.
- components: the per-component token slots.