Colour tokens: primitive and semantic
Two layers of tokens
Colour tokens usually come in two layers. Primitive tokens are the raw palette, such as blue-500 or gray-100. Semantic tokens describe a job, such as text-primary, surface, border, accent and danger. Components use only the semantic layer, and each semantic token points to a primitive.
Name by role and plan the states
A token called blue-button breaks the moment the brand turns green. A token called accent survives the change, because only its value moves. Role names also tell developers where a colour belongs.
- Text: text-primary, text-secondary, text-disabled
- Surfaces: surface, surface-raised
- Borders: border, border-strong
- Accent: accent, accent-hover, accent-active
- Feedback: danger, success, warning
Plan states early. Hover, active, focus and disabled each need a defined value rather than an improvised one.
Interactive elements change appearance, and each change deserves a token. A button might use accent for its default fill, accent-hover for the pointer-over state and accent-active while pressed. A disabled control uses a muted pair, such as text-disabled on a surface-muted fill.
- Hover: slightly darker or lighter than the default.
- Active: one more step than hover.
- Focus: a clearly visible ring that does not depend on colour alone.
- Disabled: lower emphasis, but disabled controls are exempt from contrast requirements, so keep the label legible anyway.
Never use colour as the only signal. Pair danger red with an icon or a text label.
Cheap dark mode and theming
When components reference only semantic tokens, a new theme means reassigning those tokens. Nothing inside the components changes. CSS custom properties make this direct:
Light theme: <code>:root { --surface: #ffffff; --text-primary: #1a1a1a; }</code>
Dark theme: <code>@media (prefers-color-scheme: dark) { :root { --surface: #1c1c1e; --text-primary: #ececec; } }</code>
Components then use <code>background: var(--surface)</code> and never a raw hex value.
Check contrast per token pair
Tokens do not guarantee readable colour. Check each pair that appears together, in every theme. WCAG asks for at least 4.5:1 for body text and 3:1 for large text and UI components such as borders of inputs.
- List the pairs that appear together: text-primary on surface, accent on surface, and text on accent.
- Measure each pair with a contrast checker.
- Record the result next to the token.
- Repeat for dark mode and for disabled states.
Document every approved pair in the design system, so contrast is checked once instead of on every screen.
Tokens also help with handoff. Developers copy a token name instead of a hex code, and a later brand update changes one value in one place. Keep the primitive palette small, usually a handful of neutrals plus one or two hues with several lightness steps each.
Sources
- W3C. Web Content Accessibility Guidelines (WCAG) 2.2. https://www.w3.org/TR/WCAG22/
- MDN Web Docs. Using CSS custom properties (variables). https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties
This guide was drafted with AI assistance from public standards and references. For the exact wording of a standard, use the sources listed above.