Every designer has been here: you pick five colors that look great in Figma, ship them to development, and within three months the palette has mutated into 47 different shades of blue because nobody defined where each color should actually be used.
The problem isn't choosing bad colors. The problem is starting with colors instead of roles. A design system color palette isn't a list of hex codes — it's a system of semantic assignments that tells every component exactly which color to use and when.
Step 1: Define Your Semantic Roles
Before choosing a single color, define the jobs your colors need to do. Every UI needs these semantic roles:
- Background — the page canvas. Usually the lightest or darkest surface.
- Surface — cards, modals, panels. Slightly elevated from background.
- Primary — your brand color. Used for CTAs, links, active states.
- Secondary — supporting accent. Less prominent than primary.
- Error — destructive actions and validation failures.
- Success — confirmations, positive states.
- Warning — cautionary states, pending actions.
- Text — primary content text (body copy).
- Text-muted — secondary labels, placeholders, timestamps.
Notice that "brand blue" isn't on this list. Your brand color maps to the Primary role, but the role exists independently of the specific color. This is what makes a design system scalable — when the brand refreshes, you update one token, not 500 components.
Step 2: Build Your Shade Scales
Each semantic role needs a range of shades so you can handle different contexts — hover states, disabled states, light-on-dark surfaces, and high-contrast accessibility requirements.
Build a 10-step shade scale for each primary color:
- 50-100 — lightest tints (backgrounds, subtle highlights)
- 200-300 — light variants (borders, dividers, disabled states)
- 400-500 — mid-range (the "main" color — your brand at its purest)
- 600-700 — darker variants (hover states, active states)
- 800-900 — darkest shades (text on light backgrounds, high-contrast mode)
For neutrals (grays), use a similar scale but with careful undertone matching. Cool brands use blue-tinted grays (#F8FAFC → #0F172A). Warm brands use brown-tinted grays (#FAFAF9 → #1C1917). Mixing warm and cool grays in the same palette creates visual noise.
Step 3: Map Roles to Shades Using Contrast Ratios
This is where most design systems skip a step. You don't just pick shade-500 for your primary color and call it done. You need to verify that each role-shade combination meets WCAG contrast requirements for its intended use.
Here's the mapping logic:
- Background: Use shade-50 (light mode) or shade-900 (dark mode).
- Primary on Background: The shade that achieves 4.5:1+ contrast against the background shade.
- Primary on Surface: Same rule, but tested against the surface shade.
- Error: Must achieve 4.5:1 against both background and surface.
- Text on Background: Must achieve 7:1 (AAA) for body copy.
Use the ihatecolors WCAG Contrast Checker to validate every combination. A color that looks beautiful at shade-500 might fail contrast at shade-400 and need to be bumped to shade-600 for text usage.
Step 4: Create Your Design Tokens
Design tokens are the bridge between design and code. Instead of referencing #3B82F6 in your components, you reference --color-primary-500. This makes your system maintainable and themeable.
A minimal token structure looks like this:
:root {
/* Semantic roles */
--color-bg: var(--color-neutral-50);
--color-surface: var(--color-neutral-100);
--color-primary: var(--color-blue-500);
--color-error: var(--color-red-500);
--color-text: var(--color-neutral-900);
--color-text-muted: var(--color-neutral-500);
/* Shade scales */
--color-blue-50: #eff6ff;
--color-blue-100: #dbeafe;
--color-blue-500: #3b82f6;
--color-blue-600: #2563eb;
--color-blue-700: #1d4ed8;
--color-blue-900: #1e3a8a;
--color-neutral-50: #f8fafc;
--color-neutral-100: #f1f5f9;
--color-neutral-500: #64748b;
--color-neutral-900: #0f172a;
--color-red-500: #ef4444;
--color-red-600: #dc2626;
} This structure means your components never reference raw hex codes. When you refresh the brand, you update --color-primary and every component adapts automatically.
Step 5: Build Dark Mode with the Same Tokens
Dark mode isn't a separate palette — it's a remapping of the same tokens. Your semantic roles stay identical; only the shade assignments change:
[data-theme="dark"] {
--color-bg: var(--color-neutral-900);
--color-surface: var(--color-neutral-800);
--color-text: var(--color-neutral-50);
--color-text-muted: var(--color-neutral-400);
--color-primary: var(--color-blue-400);
} Notice that the primary color shifts from shade-500 to shade-400 in dark mode — lighter shades have better contrast against dark backgrounds. This is the kind of shade-level adjustment that separates a good design system from a great one.
Step 6: Validate with Real Components
A color palette that looks perfect in a swatch grid can fail catastrophically in a real interface. Before finalizing your tokens:
- Build a button component with primary, secondary, and ghost variants.
- Build a card component with hover and disabled states.
- Build a form with error states, success states, and placeholder text.
- Test every combination in both light and dark mode.
- Test with color blindness simulation (protanopia, deuteranopia, tritanopia).
The ihatecolors generator can help here — generate a palette, then use the Palette Studio to see how the colors look applied to real UI mockups before you commit.
Scaling: From Startup to Enterprise
The role-first approach scales naturally. A 2-person startup might start with 6 semantic roles and 30 tokens. As the team grows:
- Add more neutral shades for fine-grained elevation.
- Add informational and attention states for complex dashboards.
- Add high-contrast tokens for accessibility compliance.
- Add surface-variant tokens for dark mode granularity.
The key principle: every new token maps to a semantic role. If you can't explain what job a color does, it shouldn't be in the system.
Summary: The Design System Color Checklist
- Define 6-8 semantic roles before choosing any hex codes.
- Build a 10-step shade scale for each brand and neutral color.
- Map roles to shades using WCAG contrast verification.
- Create CSS custom property tokens that reference shade scales.
- Build dark mode by remapping the same tokens to different shades.
- Validate with real components, not just swatch grids.
A design system color palette isn't a deliverable — it's a living system. Start with roles, validate with contrast, and let the tokens do the work.
Frequently Asked Questions
How many colors should a design system have?
A well-structured design system needs 6-8 semantic color roles (background, surface, primary, secondary, error, warning, success, text), each with 8-10 shade variants. That's roughly 50-80 total color tokens — far fewer than most people think.
What is the difference between brand colors and design tokens?
Brand colors are your marketing hex codes. Design tokens are semantic roles that map to brand colors but also include functional colors (error, success) and neutral scales. Tokens are implementation-ready; brand colors are not.
How do I create a color token system?
Start with 5-8 semantic roles, create a 10-step shade scale for each, then map each shade to the role where it passes WCAG contrast requirements. Export as CSS custom properties or Tailwind config.