Design Token Guide
Complete guide to building a scalable color system with design tokens — the 50–900 scale, semantic tokens, CSS custom properties and integration with Tailwind, Material and other frameworks.
What Are Design Tokens?
Design tokens are named values for design decisions — colors, spacing, typography — stored as variables that can be reused across a design system. For color specifically, a token is a name like --color-primary that maps to a HEX value like #155EEF.
Tokens solve three problems:
- Consistency: Every component uses the same color, even across teams and platforms.
- Theming: Change one token and every usage updates.
- Communication: "Use primary-500" is unambiguous; "use #155EEF or similar" is not.
Two Layers of Color Tokens
Primitive Tokens
Raw color values organized as scales. Typically numbered 50 to 900:
--color-primary-50: #EAF1FF;
--color-primary-100: #D6E3FF;
--color-primary-200: #B0C8FF;
--color-primary-300: #88AAFF;
--color-primary-400: #5C88FB;
--color-primary-500: #2563EB;
--color-primary-600: #155EEF;
--color-primary-700: #0F4BD1;
--color-primary-800: #0A3BAA;
--color-primary-900: #062A7A;
Semantic Tokens
Map primitives to specific roles:
--color-primary: var(--color-primary-600);
--color-primary-hover: var(--color-primary-700);
--color-background: var(--color-neutral-50);
--color-surface: var(--color-neutral-0);
--color-text: var(--color-neutral-900);
--color-text-muted: var(--color-neutral-600);
--color-border: var(--color-neutral-200);
--color-danger: var(--color-red-600);
--color-success: var(--color-green-600);
--color-warning: var(--color-amber-500);
Components reference semantic tokens, not primitives:
.button {
background: var(--color-primary);
color: var(--color-text-inverse);
}
.button:hover {
background: var(--color-primary-hover);
}
.alert-danger {
background: var(--color-danger-light);
border-color: var(--color-danger);
color: var(--color-danger-text);
}
Why Two Layers?
The primitive/semantic split gives you flexibility without changing primitives:
- Dark mode: Semantics remap to different primitives (--color-background: neutral-900 instead of neutral-50) without touching primitive values.
- White-labeling: Client A uses brand-blue-500 as primary; Client B uses brand-purple-500. Only the semantic mapping changes.
- Component consistency: Components reference
--color-primary— they do not need to know whether that is #155EEF or #7C3AED.
The 50–900 Scale
Almost every mature design system uses 50–900 for the primitive scale. The naming is not arbitrary:
| Step | Typical Use |
|---|---|
| 50 | Subtle backgrounds, tinted surfaces |
| 100 | Light backgrounds, disabled states |
| 200 | Borders on light backgrounds, badges |
| 300 | Borders, focus rings |
| 400 | Placeholder text, muted icons |
| 500 | Primary brand color (default) |
| 600 | Hover states for 500, links |
| 700 | Active/pressed states, dark UI accents |
| 800 | Strong text, dark borders |
| 900 | Darkest shade, heading text |
How the Scale Is Generated
Design systems use different formulas, but the most common approach is to work in HSL or OKLCH:
- Start with the base brand color (typically 500 or 600)
- Compute lightness steps in even intervals
- Slightly desaturate toward the extremes (near white and near black)
- Ensure each step has 8–12% lightness difference from its neighbors
OmniColor's Design Token Generator does this automatically from any base color, producing a full 50–900 scale.
Neutral Tokens
Neutrals are typically a separate scale, often tinted with the brand hue:
--color-neutral-0: #FFFFFF;
--color-neutral-50: #F8FAFF;
--color-neutral-100: #F0F4FF;
--color-neutral-200: #E3EAF5;
--color-neutral-300: #D1DAE6;
--color-neutral-400: #A0AEC0;
--color-neutral-500: #7A8699;
--color-neutral-600: #4A5A75;
--color-neutral-700: #334155;
--color-neutral-800: #1E293B;
--color-neutral-900: #15243B;
Tinting neutrals with the brand hue (even at 3–5% saturation) creates a cohesive system that feels intentional. Pure grays often look disconnected from the rest of the palette.
Dark Mode with Tokens
The cleanest way to add dark mode is to remap semantic tokens while keeping primitives unchanged:
:root {
--color-background: var(--color-neutral-50);
--color-surface: var(--color-neutral-0);
--color-text: var(--color-neutral-900);
--color-text-muted: var(--color-neutral-600);
--color-border: var(--color-neutral-200);
}
[data-theme="dark"] {
--color-background: var(--color-neutral-900);
--color-surface: var(--color-neutral-800);
--color-text: var(--color-neutral-50);
--color-text-muted: var(--color-neutral-400);
--color-border: var(--color-neutral-700);
}
Components do not need to change at all — they already use var(--color-background), which now returns a dark value.
Framework Integration
CSS Custom Properties
Simplest approach — just use var(--color-primary) anywhere in your CSS.
Tailwind CSS
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: {
50: 'var(--color-primary-50)',
500: 'var(--color-primary-500)',
600: 'var(--color-primary-600)',
700: 'var(--color-primary-700)',
900: 'var(--color-primary-900)',
}
}
}
}
}
Material UI
// theme.js
export const theme = createTheme({
palette: {
primary: {
main: '#155EEF',
light: '#2563EB',
dark: '#0F4BD1',
contrastText: '#FFFFFF',
},
background: {
default: '#F8FAFF',
paper: '#FFFFFF',
},
},
});
Naming Conventions
Common naming patterns:
- Numeric scale:
--color-primary-500 - Semantic:
--color-primary,--color-danger - Role-based:
--color-background,--color-surface,--color-text - Component-based:
--button-primary-bg(less common — usually semantic is preferred)
Avoid:
- Naming tokens after their value:
--color-blue(breaks if brand shifts to red) - Overly specific names:
--color-primary-button-hover-border(creates token sprawl) - Mixing naming conventions in one system
Common Mistakes
Too many tokens
Some systems have 500+ color tokens. This is almost always wrong. Start with 50–100 tokens for a complete system.
Primitive tokens used directly in components
If a component uses var(--color-primary-500) instead of var(--color-primary), you lose the ability to re-theme. Always reference semantic tokens from components.
No accessibility validation
Tokens should be tested for WCAG contrast in their primary combinations. Our contrast checker verifies token combinations.
Ignoring dark mode from the start
Adding dark mode after building light mode requires rearchitecting tokens. Build the semantic layer with dark mode in mind from day one.
Building Your Token System
- Pick one base color. Your brand color, ideally at the 500 or 600 step.
- Generate a 50–900 scale. Use our Design Token Generator.
- Create neutrals. Either pure grays or tinted neutrals based on your brand hue.
- Define semantic tokens. Map primitives to roles: primary, background, surface, text, border, success, warning, danger.
- Test contrast. Verify every semantic combination passes WCAG AA.
- Add dark mode. Remap semantics to dark primitives.
- Document. Create a style guide page showing each token and where to use it.
- Export. Generate CSS custom properties for your framework.
Related Tools
- Design Token Generator — full 50–900 scale + semantic tokens
- CSS Variables Generator — website-specific tokens
- UI Palette Generator — 50–900 scale from a base color
- Palette Exporter — export tokens as CSS, JSON, Tailwind
- Tailwind Reference — see Tailwind's token structure