Design Tokens — colors, spacing, typography as CSS custom properties
13 min read

Design Tokens: The Foundation of Every Design System

#CSS #Design Tokens #Design Systems

You would never hardcode a database connection string in 47 different files. So why do frontend teams still hardcode #3b82f6 in 200 places across their stylesheets? Design tokens solve this problem the same way configuration solves it in backend development: one source of truth, referenced everywhere, changed in one place.

Design tokens are the visual atoms of a design system. They store decisions — not just values — about color, typography, spacing, shadows, borders, and motion in a format that every component can consume. If you are new to the concept of design systems, my guide on getting started with Atomic Design explains the component hierarchy that tokens power.

Design tokens are the visual design atoms of the design system — specifically, they are named entities that store visual design attributes.

Jina Anne Creator of Design Tokens Community Group

Why Design Tokens Matter

1
Source of Truth
Token file
200+
Components Synced
Automatically
0
Hardcoded Values
Goal

Without tokens, design decisions are scattered across CSS files, component styles, and inline styles. A “brand blue” might be #2563eb in the header, #3b82f6 in buttons, and #1d4ed8 in links — three different blues that were meant to be the same. Tokens eliminate this drift by establishing a single canonical value.

When you update a token, every component that references it updates automatically. Change --color-primary once, and your buttons, links, headings, focus rings, and active states all follow. This is the foundational promise of a design system. To see how tokens integrate with a real component library of 45+ components, read about Atomic Design in practice.

Token Categories

Color token palette visualization
Color Tokens

Your entire color palette — from brand primaries and neutrals to semantic colors for success, warning, error, and info states. Semantic tokens like --color-text and --color-bg adapt between light and dark modes without changing component code. Define primitive tokens (the raw palette) and semantic tokens (the meaning) separately.

Spacing grid system visualization
Spacing Tokens

An 8-point grid system ensures consistent spatial rhythm. Values like --space-1 (4px) through --space-12 (96px) create harmonious layouts. Components never use arbitrary pixel values — every margin, padding, and gap references a spacing token. This eliminates the "should this be 14px or 16px?" debate forever.

Typography scale demonstration
Typography Tokens

Font families, sizes, weights, line heights, and letter spacing — all tokenized. Fluid typography scales use clamp() to ensure readable text from mobile to ultrawide. A heading that is 2rem on mobile smoothly grows to 3rem on desktop without media queries.

Shadow and motion token examples
Effects & Motion

Shadows, border radii, transitions, and animations. Consistent shadows (--shadow-sm, --shadow-md, --shadow-lg) create depth hierarchy. Transition tokens (--transition-fast: 150ms, --transition-base: 300ms) ensure uniform animation feel. Border radius tokens prevent the "some buttons are 4px, some are 8px" problem.

Token Architecture: Three Layers

Design tokens work best when organized in three layers: primitive, semantic, and component. Each layer adds meaning.

Primitive tokens are the raw values — your palette. They have no meaning attached, just names and values. --blue-500: #3b82f6, --gray-100: #f3f4f6, --space-4: 1rem. You never use these directly in components. They are the building blocks for semantic tokens.

Semantic tokens assign meaning to primitives. --color-primary: var(--blue-500), --color-bg: var(--gray-100), --color-text: var(--gray-900). Components use semantic tokens exclusively. This is how dark mode works: you swap the primitive values behind semantic tokens, and every component adapts.

Component tokens (optional) are aliases for specific components. --btn-bg: var(--color-primary), --btn-text: var(--color-on-primary), --card-radius: var(--radius-md). They add an extra layer of flexibility: you can change all button backgrounds without affecting other components that use --color-primary.

Implementation in CSS

/* tokens/primitives.css — raw palette, never used directly in components */
:root {
  --blue-50: #eff6ff;
  --blue-500: #3b82f6;
  --blue-600: #2563eb;
  --blue-700: #1d4ed8;
  --gray-50: #f9fafb;
  --gray-100: #f3f4f6;
  --gray-700: #374151;
  --gray-900: #111827;
  --space-1: 0.25rem;   /* 4px */
  --space-2: 0.5rem;    /* 8px */
  --space-4: 1rem;      /* 16px */
  --space-6: 1.5rem;    /* 24px */
  --space-8: 2rem;      /* 32px */
}
/* tokens/semantic.css — meaningful names, used in all components */
:root {
  --color-primary: var(--blue-600);
  --color-primary-hover: var(--blue-700);
  --color-bg: var(--gray-50);
  --color-bg-alt: var(--gray-100);
  --color-text: var(--gray-900);
  --color-text-light: var(--gray-700);
  --font-size-base: 1rem;
  --font-size-lg: 1.25rem;
  --font-size-xl: 1.5rem;
  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
  --shadow-md: 0 4px 6px rgba(0,0,0,0.1);
  --transition-fast: 150ms ease;
  --transition-base: 300ms ease;
}
/* Dark mode — swap primitives behind semantic tokens */
[data-theme="dark"] {
  --color-primary: var(--blue-500);
  --color-primary-hover: var(--blue-600);
  --color-bg: var(--gray-900);
  --color-bg-alt: var(--gray-700);
  --color-text: var(--gray-50);
  --color-text-light: var(--gray-100);
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
  --shadow-md: 0 4px 6px rgba(0,0,0,0.4);
}
/* Components use the same tokens — zero code changes */

Tokens vs. Alternatives

Approach Maintainability Recommended Dark Mode Performance
CSS Custom Properties Excellent Native swap Zero runtime
Tailwind Utility Classes Good Config-based Purge required
CSS-in-JS (styled-components) Moderate Theme provider Runtime cost
SCSS Variables Moderate Manual duplication Build-time only
Hardcoded Values Terrible Impossible N/A

Setting Up Your Token System

Token Implementation Guide

  1. Audit Existing Styles

    Search your codebase for hardcoded color values, pixel spacings, and font declarations. List every unique value — you will likely find duplicates and near-duplicates that should be consolidated.

  2. Define Primitive Tokens

    Create your raw palette in tokens/primitives.css. Colors (full scales like blue-50 through blue-900), spacing (4px increments), font sizes, font families, and font weights.

  3. Create Semantic Tokens

    Map primitives to meaningful names in tokens/semantic.css. --color-primary, --color-text, --color-bg, --color-error, --color-success. These are what your components will actually use.

  4. Replace Hardcoded Values

    Go through every component and replace raw values with token references. color: #3b82f6 becomes color: var(--color-primary). padding: 16px becomes padding: var(--space-4).

  5. Add Dark Mode

    Create a [data-theme="dark"] block that reassigns semantic tokens to different primitives. Toggle the data-theme attribute on <html> with a small script. Every component adapts automatically.

Real-World Token File Structure

src/styles/
├── global.css              → Imports all token files, base resets
└── tokens/
    ├── primitives.css      → Raw palette (colors, scales)
    ├── semantic.css        → Meaningful names (light mode defaults)
    ├── dark.css            → Dark mode overrides
    ├── typography.css      → Font families, sizes, weights, line heights
    ├── spacing.css         → 8-point grid values
    └── effects.css         → Shadows, radii, transitions

The Power of Cascading Tokens

One of the most powerful patterns is contextual token overrides. Since CSS custom properties cascade, you can override tokens in specific contexts:

/* A dark card overrides tokens for its children */
.card--dark {
  --color-bg: var(--gray-900);
  --color-text: var(--gray-50);
  --color-primary: var(--blue-400);
}
/* All children (headings, text, links) automatically adapt */
/* Adjust spacing tokens for mobile */
@media (max-width: 768px) {
  :root {
    --space-section: var(--space-6);  /* 24px instead of 48px */
    --font-size-hero: var(--font-size-xl); /* smaller hero text */
  }
}
/* A hero section with different visual treatment */
.hero {
  --color-bg: var(--color-primary);
  --color-text: white;
}
/* All organisms inside the hero inherit these overrides */

Benefits at Scale

Brand Consistency

Every component draws from the same palette. No more "which blue was that again?" conversations.

Dark Mode for Free

Swap token values in one file. Every component adapts without any modifications.

Accessibility Built-in

Enforce contrast ratios at the token level. --color-text on --color-bg always meets WCAG 4.5:1.

Spatial Harmony

The 8-point grid eliminates arbitrary spacing. Every layout has consistent rhythm and breathing room.

Easy Rebranding

Changing the entire visual identity means updating one token file. No component changes needed.

Design-Dev Sync

Token names in code match token names in Figma. Designers and developers speak the same language.

Common Mistakes

Frequently Asked Questions