Design Tokens — color
Design tokens are named values of the visual language. Instead of “that blue somewhere in CSS,” you get a contract: color.primary.default, understood the same way by design, web, and — when needed — native.
For color this is especially critical: inconsistency spreads fastest here, and accessibility breaks here first.
Why tokens instead of hex in components
// Bad: magic value
<button style={{ backgroundColor: '#3B82F6' }} />
// Better: token
<button style={{ backgroundColor: 'var(--color-action-primary)' }} />
Benefits:
- one source of truth;
- easier themes (light/dark/brand);
- easier audits and cross-platform consistency;
- palette changes do not require hunting the whole repository.
Three layers of color tokens
1. Primitive (raw palette)
export const color = {
blue: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
900: '#1e3a8a'
},
gray: {
50: '#f9fafb',
500: '#6b7280',
900: '#111827'
}
};
2. Semantic (meaning in the product)
export const semanticColor = {
action: {
primary: '{color.blue.500}',
primaryHover: '{color.blue.600}'
},
text: {
primary: '{color.gray.900}',
secondary: '{color.gray.500}'
},
feedback: {
success: '#16a34a',
danger: '#dc2626',
warning: '#d97706'
}
};
3. Component tokens (optional)
button.primary.bg → semantic.action.primary. Needed when variants multiply and the shared semantic layer is no longer enough.
Components should consume semantic (or component) tokens, not primitive blue.500 directly — otherwise a brand change turns into a repo-wide search.
How to build a palette
- Lock brand colors from the guidelines.
- Build a 50–900 scale (or a smaller but regular set of steps).
- Add neutrals for text, backgrounds, and borders.
- Add feedback: success / warning / danger / info.
- Check contrast of key pairs.
The neutral scale usually matters more than a “pretty accent”: it carries typography, forms, and tables.
Contrast and a11y
WCAG AA minimum:
- normal text — 4.5:1;
- large text and meaningful UI elements — 3:1.
Practical rules:
- primary text on background — high contrast (
gray.900on white); - secondary text must still be readable, not “almost gray on gray”;
- do not rely on color alone for errors — add text/icon;
- check the primary button: background + text + focus state.
Tools: WebAIM Contrast Checker, DevTools, axe.
A text.disabled token may be intentionally low-contrast, but interactive elements that look disabled without another state cue are a common a11y mistake.
CSS variables as the delivery layer
:root {
--color-bg: #ffffff;
--color-text: #111827;
--color-action-primary: #2563eb;
--color-border: #e5e7eb;
}
[data-theme="dark"] {
--color-bg: #0b1220;
--color-text: #f8fafc;
--color-action-primary: #60a5fa;
--color-border: #334155;
}
.button-primary {
background: var(--color-action-primary);
color: #fff;
}
This approach pairs well with both Tailwind theme and CSS Modules: JS tokens stay the source, CSS variables are the runtime contract.
Dark mode without pain
Do not copy an “inverted rainbow.” Switch semantic tokens:
- background / surface / elevated;
- text primary/secondary;
- borders and shadows;
- action colors (sometimes lighter on dark backgrounds for contrast).
The primitive palette can stay shared; what changes is the semantic → primitive mapping.
Lesson takeaway
A design-system color system = palette + semantic mapping + contrast checks + delivery via variables/theme. If you skip the semantic layer, you do not have tokens — you have a list of hex codes.