Color System Design — Build a Scalable Color System
How to design a color system that scales from one page to hundreds of screens without visual drift.
Why Color Systems Matter
A color system is the foundation of any design system. It defines the palette, the semantic meaning of each color, the state variations and the rules for usage. Without a system, colors proliferate — different designers choose slightly different blues, borders become inconsistent, and the product gradually loses visual coherence.
With a system, color decisions are made once and applied consistently everywhere. New components inherit the correct colors automatically. Rebranding becomes a single change rather than hundreds.
The Layers of a Color System
Layer 1: Primitives
The raw color values — the actual HEX/RGB codes. These are usually organized in scales:
- Blue 50, 100, 200, ..., 900
- Red 50, 100, 200, ..., 900
- Neutral 50, 100, 200, ..., 900
Primitives are the "atoms" — they have no semantic meaning. They're just colors.
Layer 2: Semantic Tokens
Tokens map primitives to functional roles:
- primary = blue-500
- primary-hover = blue-600
- background = neutral-50
- surface = neutral-100
- text = neutral-900
- error = red-500
Semantic tokens are the "molecules" — they have meaning but can be remapped.
Layer 3: Component Tokens
Optional layer: component-specific tokens that reference semantic tokens:
- button-primary-bg = primary
- button-primary-text = white
- card-bg = surface
- card-border = border
This layer allows per-component customization without breaking the semantic system.
Building the Scale
How Many Colors?
Most design systems use:
- 1 primary brand color (with 10 shades)
- 1-2 secondary colors
- 1 success (green)
- 1 warning (amber)
- 1 error (red)
- 1 info (blue)
- 1 neutral scale (10 shades)
That's 7-8 color families × 10 shades = 70-80 base colors — enough for nearly any UI.
Scale Spacing
The 50-900 naming comes from the observation that most UIs need about 10 lightness steps. Some design systems use more (Radix uses 12 steps), some fewer. Ten is a good default.
Within each scale, lightness should decrease approximately evenly from step 50 (very light) to step 900 (very dark).
Saturation Variation
Professional scales vary saturation alongside lightness:
- Light steps (50-200) are less saturated — they're close to white
- Mid steps (400-600) have maximum saturation
- Dark steps (700-900) are slightly desaturated — they're close to black
This produces scales that look natural rather than garish.
Semantic Token Design
Name by Purpose, Not Color
Good token names:
- color-primary
- color-background
- color-text
- color-error
Bad token names:
- color-blue (what if primary becomes green?)
- color-light-gray (what if the theme changes?)
Include State Variants
Every interactive color needs states:
- color-primary (default)
- color-primary-hover
- color-primary-active
- color-primary-disabled
Theming
Semantic tokens enable theming without touching component code. To create a dark theme:
- Remap background → neutral-900 instead of neutral-50
- Remap text → neutral-50 instead of neutral-900
- Remap primary → primary-400 instead of primary-500 (lighter for dark backgrounds)
Components don't change — they just read the tokens, which now resolve to different values.
Common Pitfalls
Pitfall 1: Too Many Colors
Design systems that start with 15 brand colors and 5 shades each quickly become unmanageable. Start with fewer colors and add only when necessary.
Pitfall 2: Inconsistent Scales
If blue-500 is #155EEF but red-500 has completely different lightness, mixing them produces inconsistent hierarchy. All scales should have similar lightness at each step.
Pitfall 3: No Accessibility Testing
Colors that look fine in isolation can fail WCAG contrast when combined. Test every pairing.
Pitfall 4: Ignoring Dark Mode
Colors designed for light mode often fail in dark mode. Design both simultaneously.
Implementing in CSS
:root {
/* Primitives */
--blue-500: #155EEF;
--blue-600: #0F4BD1;
--neutral-50: #F8FAFF;
--neutral-900: #15243B;
/* Semantic tokens */
--color-primary: var(--blue-500);
--color-primary-hover: var(--blue-600);
--color-background: var(--neutral-50);
--color-text: var(--neutral-900);
}
[data-theme="dark"] {
--color-background: var(--neutral-900);
--color-text: var(--neutral-50);
}