Tokens
Token architecture
Three layers, each answering a different question. The separation exists so that changing how something looks and changing what it means are two different edits.
The three layersPermalink to this section
A value moves left to right: a raw number becomes an intent, and an intent becomes a component's contract.
Primitive
A raw value
Semantic
What it means
Component
Where it applies
Hover or focus a row to trace how a raw value reaches a component.
What each layer is forPermalink to this section
The layers are not arbitrary. Each one is the answer to a question the layer below it cannot answer.
Primitive
Raw values. No meaning, no opinion about where they are used.
95 tokens · 11 groups
Semantic
Intent. What a value means, independent of which component uses it.
56 tokens · 9 groups
Component
Contracts. Per-component hooks for targeted overrides.
231 tokens · 10 groups
RulesPermalink to this section
Four rules keep the layers meaningful. Break them and the architecture becomes decoration.
- Components consume semantic or component tokens only. A component that names brand-600 has hardcoded a brand decision.
- Semantic tokens reference primitives, never other semantic tokens. Two-step aliasing makes the resolved value impossible to predict.
- A component token exists when a component genuinely needs independent retheming — not merely because it uses a colour.
- Every token name says what it is for, not what it looks like. brand-primary survives a rebrand; purple-button does not.
Layer referencePermalink to this section
Where each layer lives, who may read it, and what changing it affects.
| Layer | Example | Consumed by | Changing it affects |
|---|---|---|---|
| Primitive | --brand-600 | Semantic tokens only | Everything downstream — a rebrand |
| Semantic | --brand-primary | Components and product code | Every component expressing that intent |
| Component | --button-primary-background | One component | That component only |
Why not just use the primitive?Permalink to this section
Because the primitive cannot express intent, and intent is what survives a redesign.
background: var(--brand-primary);Do — name the role
The declaration says what the colour is doing. Reassigning the brand updates it automatically.
background: var(--brand-600);Don't — name the value
Now every consumer has to be found and edited by hand when the brand changes — and some will be missed.
In codePermalink to this section
The layers exist as CSS custom properties. Tailwind's theme maps the semantic layer onto utility classes, so most product code never writes var() at all.
// primitives.ts — a raw value, no meaning.
"brand-600": { value: "#c94e0c", description: "Brand base." }
// semantic.ts — what the value is for.
"brand-primary": {
value: { light: "brand-600", dark: "brand-400" },
description: "Primary action background.",
}
// components.ts — where it applies.
"button-primary-background": {
value: "brand-primary",
description: "Primary variant fill.",
}Dark modePermalink to this section
Light and dark values are authored together and emitted from the same registry. The saved preference is applied before hydration, so the first painted frame is already correct.
One attribute to switch
Every component reads semantic and component tokens. The theme provider changes data-theme on the root element; no component receives a theme prop and no component file changes.
background: {
value: { light: "neutral-0", dark: "neutral-950" },
description: "The page canvas.",
},
"surface-raised": {
value: { light: "neutral-0", dark: "neutral-900" },
description: "Cards and overlays above the canvas.",
},
"brand-primary": {
value: { light: "brand-600", dark: "brand-500" },
description: "Primary action background.",
}