Design Tokens

Difficulty: Advanced

Design tokens are the atomic, reusable values that define a design system's visual language. They are named entities that store design decisions such as colors, typography, spacing, border radii, shadows, and animation timings. Instead of hardcoding hex colors and pixel values throughout your CSS, you define tokens like --color-primary, --space-md, and --radius-lg, then reference them everywhere. When the brand color changes, you update one token and every component updates automatically. Design tokens are the single source of truth that bridges design tools, CSS, and even native platforms.

CSS custom properties (CSS variables) are the native web implementation of design tokens. Declared with the -- prefix and accessed via var(), they cascade through the DOM tree like regular CSS properties, making them ideal for theming. You can define tokens on :root for global scope, override them on specific elements or classes for local customization, and change them with JavaScript for runtime theme switching. Unlike preprocessor variables (SASS $variables), CSS custom properties are live values that exist at runtime and can be modified dynamically without recompilation.

A well-structured token system follows a hierarchy with three levels. Primitive tokens (also called global or base tokens) are raw values with abstract names: --color-blue-500, --font-size-16, --space-4. Semantic tokens (also called alias tokens) map primitives to purposes: --color-text-primary, --color-bg-surface, --button-padding. Component tokens are specific to individual components: --card-border-radius, --input-height, --nav-bg. This layered approach means changing the meaning of 'primary color' updates a single semantic token, while all components using that token automatically update.

Theming systems are built on top of design tokens by swapping token values based on context. The most common pattern is dark/light mode: you define a set of semantic tokens, then override their values when a .dark-theme class is applied to the document. Because CSS custom properties cascade, overriding a token on a parent element changes its value for all descendants. This same mechanism supports multiple themes (brand themes, high contrast mode, compact/comfortable density), user-customized accent colors, and per-section theming within a single page.

Design tokens extend beyond CSS. Tools like Style Dictionary, Tokens Studio, and Amazon Style Dictionary transform token definitions (typically stored as JSON or YAML) into platform-specific outputs: CSS custom properties for web, XML resources for Android, Swift constants for iOS, and SCSS variables for legacy systems. This multi-platform approach ensures visual consistency across web, mobile, and desktop applications. The token definition file becomes the single source of truth that design tools (Figma), development environments, and documentation systems all reference.

Code examples

Design token hierarchy with CSS custom properties

:root {
  /* ===== PRIMITIVE TOKENS (raw values) ===== */
  /* Colors */
  --color-blue-50: #eff6ff;
  --color-blue-100: #dbeafe;
  --color-blue-500: #3b82f6;
  --color-blue-600: #2563eb;
  --color-blue-700: #1d4ed8;

  --color-gray-50: #f9fafb;
  --color-gray-100: #f3f4f6;
  --color-gray-200: #e5e7eb;
  --color-gray-500: #6b7280;
  --color-gray-700: #374151;
  --color-gray-900: #111827;

  --color-red-500: #ef4444;
  --color-green-500: #22c55e;

  /* Spacing scale */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;

  /* Typography scale */
  --text-xs: 12px;
  --text-sm: 14px;
  --text-base: 16px;
  --text-lg: 18px;
  --text-xl: 20px;
  --text-2xl: 24px;
  --text-3xl: 30px;

  /* Border radii */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-full: 9999px;

  /* ===== SEMANTIC TOKENS (purpose-mapped) ===== */
  --color-text-primary: var(--color-gray-900);
  --color-text-secondary: var(--color-gray-500);
  --color-text-inverse: #fff;

  --color-bg-page: #fff;
  --color-bg-surface: var(--color-gray-50);
  --color-bg-elevated: #fff;

  --color-border: var(--color-gray-200);
  --color-interactive: var(--color-blue-500);
  --color-interactive-hover: var(--color-blue-600);
  --color-error: var(--color-red-500);
  --color-success: var(--color-green-500);

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
  --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
}

/* Usage in components */
.card {
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  box-shadow: var(--shadow-sm);
}

.card-title {
  color: var(--color-text-primary);
  font-size: var(--text-lg);
}

.button-primary {
  background: var(--color-interactive);
  color: var(--color-text-inverse);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-md);
}

.button-primary:hover {
  background: var(--color-interactive-hover);
}

Primitive tokens are raw design values (colors, sizes). Semantic tokens map primitives to purposes (text-primary, bg-surface, interactive). Components reference semantic tokens, never primitives directly. This indirection means changing the brand color or theme only requires updating semantic token mappings.

Dark mode theming with token overrides

:root {
  /* Light theme (default) */
  --color-text-primary: #111827;
  --color-text-secondary: #6b7280;
  --color-text-inverse: #ffffff;
  --color-bg-page: #ffffff;
  --color-bg-surface: #f9fafb;
  --color-bg-elevated: #ffffff;
  --color-border: #e5e7eb;
  --color-interactive: #3b82f6;
  --color-interactive-hover: #2563eb;
  --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
}

/* Dark theme: override semantic tokens */
.dark-theme {
  --color-text-primary: #f9fafb;
  --color-text-secondary: #9ca3af;
  --color-text-inverse: #111827;
  --color-bg-page: #111827;
  --color-bg-surface: #1f2937;
  --color-bg-elevated: #374151;
  --color-border: #4b5563;
  --color-interactive: #60a5fa;
  --color-interactive-hover: #93c5fd;
  --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3);
}

/* Components use tokens -- no dark mode specific styles needed */
body {
  background: var(--color-bg-page);
  color: var(--color-text-primary);
}

.card {
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-md);
  border-radius: 12px;
  padding: 16px;
}

.card-title {
  color: var(--color-text-primary);
}

.card-subtitle {
  color: var(--color-text-secondary);
}

.btn {
  background: var(--color-interactive);
  color: var(--color-text-inverse);
}

/* Toggle theme with JavaScript */
<script>
  const toggle = document.getElementById('theme-toggle');
  toggle.addEventListener('click', () => {
    document.documentElement.classList.toggle('dark-theme');
  });
</script>

<!-- Or use prefers-color-scheme for system preference -->
<style>
  @media (prefers-color-scheme: dark) {
    :root {
      --color-text-primary: #f9fafb;
      --color-bg-page: #111827;
      /* ... all dark overrides ... */
    }
  }
</style>

The dark theme overrides semantic token values on the .dark-theme class. Because CSS custom properties cascade, all components inside .dark-theme automatically use the dark values without any component-level changes. The @media query approach applies the theme based on the user's OS setting.

Component-level tokens for customization

/* Component tokens with fallbacks to semantic tokens */
.button {
  /* Component tokens default to semantic tokens */
  --btn-bg: var(--color-interactive);
  --btn-color: var(--color-text-inverse);
  --btn-padding-x: var(--space-4);
  --btn-padding-y: var(--space-2);
  --btn-radius: var(--radius-md);
  --btn-font-size: var(--text-sm);

  /* Use component tokens in styles */
  background: var(--btn-bg);
  color: var(--btn-color);
  padding: var(--btn-padding-y) var(--btn-padding-x);
  border-radius: var(--btn-radius);
  font-size: var(--btn-font-size);
  border: none;
  cursor: pointer;
  font-weight: 500;
}

.button:hover {
  filter: brightness(0.9);
}

/* Variants override component tokens */
.button-secondary {
  --btn-bg: var(--color-bg-surface);
  --btn-color: var(--color-text-primary);
}

.button-danger {
  --btn-bg: var(--color-error);
  --btn-color: #fff;
}

.button-large {
  --btn-padding-x: var(--space-6);
  --btn-padding-y: var(--space-3);
  --btn-font-size: var(--text-base);
}

/* Override from parent context */
.dialog-footer {
  /* All buttons inside dialog footer get smaller padding */
  --btn-padding-x: var(--space-3);
  --btn-padding-y: var(--space-1);
}

/* Per-instance customization via inline style */
<!-- <button class="button" style="--btn-bg: purple;">Custom</button> -->

Component tokens are CSS custom properties scoped to a component that default to semantic tokens. Variants override specific component tokens. Parent elements can override component tokens for all descendants. Inline styles can set individual tokens for per-instance customization. This creates a powerful, flexible theming API.

Token definition file (JSON) for multi-platform output

/* tokens.json - Single source of truth */
/*
{
  "color": {
    "primitive": {
      "blue": {
        "500": { "value": "#3b82f6" },
        "600": { "value": "#2563eb" }
      },
      "gray": {
        "50":  { "value": "#f9fafb" },
        "900": { "value": "#111827" }
      }
    },
    "semantic": {
      "text": {
        "primary":   { "value": "{color.primitive.gray.900}" },
        "secondary": { "value": "{color.primitive.gray.500}" }
      },
      "bg": {
        "page":    { "value": "#ffffff" },
        "surface": { "value": "{color.primitive.gray.50}" }
      },
      "interactive": { "value": "{color.primitive.blue.500}" }
    }
  },
  "space": {
    "1": { "value": "4px" },
    "2": { "value": "8px" },
    "4": { "value": "16px" }
  }
}
*/

/* Build tool (Style Dictionary) transforms to: */

/* CSS output */
:root {
  --color-text-primary: #111827;
  --color-text-secondary: #6b7280;
  --color-bg-page: #ffffff;
  --color-bg-surface: #f9fafb;
  --color-interactive: #3b82f6;
  --space-1: 4px;
  --space-2: 8px;
  --space-4: 16px;
}

/* JavaScript/TypeScript output */
// export const colorTextPrimary = '#111827';
// export const colorInteractive = '#3b82f6';
// export const space4 = '16px';

/* iOS Swift output */
// extension UIColor {
//   static let textPrimary = UIColor(hex: "#111827")
//   static let interactive = UIColor(hex: "#3b82f6")
// }

/* Android XML output */
// <color name="text_primary">#111827</color>
// <color name="interactive">#3b82f6</color>

Design tokens are defined in a platform-agnostic format (JSON/YAML). Build tools like Style Dictionary transform them into platform-specific outputs: CSS custom properties, JavaScript constants, Swift extensions, Android XML resources. One token file drives consistent styling across web, iOS, and Android.

Key points

Concepts covered

Design Tokens, CSS Custom Properties, Theming, Design Systems, Token Hierarchy, Multi-Platform Tokens