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
50Subtle backgrounds, tinted surfaces
100Light backgrounds, disabled states
200Borders on light backgrounds, badges
300Borders, focus rings
400Placeholder text, muted icons
500Primary brand color (default)
600Hover states for 500, links
700Active/pressed states, dark UI accents
800Strong text, dark borders
900Darkest 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:

  1. Start with the base brand color (typically 500 or 600)
  2. Compute lightness steps in even intervals
  3. Slightly desaturate toward the extremes (near white and near black)
  4. 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

  1. Pick one base color. Your brand color, ideally at the 500 or 600 step.
  2. Generate a 50–900 scale. Use our Design Token Generator.
  3. Create neutrals. Either pure grays or tinted neutrals based on your brand hue.
  4. Define semantic tokens. Map primitives to roles: primary, background, surface, text, border, success, warning, danger.
  5. Test contrast. Verify every semantic combination passes WCAG AA.
  6. Add dark mode. Remap semantics to dark primitives.
  7. Document. Create a style guide page showing each token and where to use it.
  8. Export. Generate CSS custom properties for your framework.

Related Tools

Frequently Asked Questions

Open Design Token Generator →