Introduction
Design tokens are the atomic building blocks of a design systemβnamed values that represent visual design attributes like colors, typography, spacing, and shadows. Instead of hardcoding #3B82F6 in every component, you define color.primary.600 as a token and reference it everywhere. When the brand color changes, you update one token and every component reflects the change.
Design tokens bridge the gap between designers and developers. Tools like Figma export tokens directly from design files, and build systems like Style Dictionary transform those tokens into platform-specific formatsβCSS custom properties, Tailwind config, iOS Swift constants, Android XML resources, and more. This creates a single source of truth that keeps design and code in sync.
The concept was pioneered by Salesforce's Lightning Design System in 2014 and has since been adopted by every major design systemβfrom Material Design to IBM Carbon to GitHub Primer. This guide covers how to implement design tokens from naming conventions to multi-platform export.
Understanding Design Tokens: Core Concepts
What Are Design Tokens?
A design token is a key-value pair that represents a single design decision:
{
"color": {
"primary": {
"500": { "value": "#3B82F6" },
"600": { "value": "#2563EB" },
"700": { "value": "#1D4ED8" }
}
},
"spacing": {
"sm": { "value": "8px" },
"md": { "value": "16px" },
"lg": { "value": "24px" }
},
"typography": {
"fontSize": {
"sm": { "value": "14px" },
"base": { "value": "16px" },
"lg": { "value": "18px" }
}
}
}This JSON file becomes the single source of truth. From it, Style Dictionary generates:
- CSS:
--color-primary-500: #3B82F6; - Tailwind:
colors.primary.500: '#3B82F6' - SCSS:
$color-primary-500: #3B82F6; - JavaScript:
colors.primary[500] - iOS:
UIColor *primary500 = [UIColor colorWithRed:0.23 green:0.51 blue:0.96]; - Android:
<color name="primary_500">#3B82F6</color>
Token Categories
Design tokens fall into three tiers:
Global Tokens (primitive values): Raw color palette, font sizes, spacing scale
{ "blue": { "500": { "value": "#3B82F6" } } }Alias Tokens (semantic references): Map global tokens to meaning
{ "color": { "primary": { "value": "{blue.500}" } } }Component Tokens (specific usage): Map alias tokens to components
{ "button": { "primary": { "bg": { "value": "{color.primary}" } } } }The Design Token Specification
The W3C Design Tokens Community Group is standardizing the token format. The emerging specification uses this structure:
{
"color": {
"primary": {
"$type": "color",
"$value": "#3B82F6",
"$description": "Primary brand color"
}
}
}The $ prefix distinguishes token metadata from nested token groups.
Architecture and Design Patterns
Naming Conventions
A consistent naming convention is critical for token discoverability:
{category}-{property}-{variant}-{state}
Examples:
color-primary-600
color-background-surface
spacing-md
typography-fontSize-lg
shadow-elevation-2
borderRadius-lg
Recommended categories:
colorβ All color valuesspacingβ Margin, padding, gap valuestypographyβ Font family, size, weight, line heightborderβ Border width, radius, styleshadowβ Box shadow valuesopacityβ Transparency valueszIndexβ Stacking ordermotionβ Animation duration, easing
Token File Structure
tokens/
βββ global/
β βββ colors.json
β βββ spacing.json
β βββ typography.json
β βββ shadows.json
βββ alias/
β βββ color.json
β βββ spacing.json
β βββ typography.json
βββ component/
β βββ button.json
β βββ input.json
β βββ card.json
βββ themes/
βββ light.json
βββ dark.json
Reference and Aliasing
Tokens can reference other tokens, creating a dependency graph:
// global/colors.json
{
"blue": {
"50": { "value": "#EFF6FF" },
"500": { "value": "#3B82F6" },
"600": { "value": "#2563EB" }
},
"gray": {
"50": { "value": "#F9FAFB" },
"900": { "value": "#111827" }
}
}
// alias/color.json
{
"color": {
"primary": { "value": "{blue.500}" },
"primary-hover": { "value": "{blue.600}" },
"background": { "value": "{gray.50}" },
"text": { "value": "{gray.900}" }
}
}Step-by-Step Implementation
Setting Up Style Dictionary
npm install style-dictionaryCreate the configuration:
// style-dictionary.config.js
const StyleDictionary = require('style-dictionary');
module.exports = {
source: ['tokens/**/*.json'],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'dist/css/',
files: [
{
destination: 'variables.css',
format: 'css/variables',
options: {
outputReferences: true
}
}
]
},
scss: {
transformGroup: 'scss',
buildPath: 'dist/scss/',
files: [
{
destination: '_variables.scss',
format: 'scss/variables'
}
]
},
js: {
transformGroup: 'js',
buildPath: 'dist/js/',
files: [
{
destination: 'tokens.js',
format: 'javascript/es6'
}
]
},
json: {
transformGroup: 'js',
buildPath: 'dist/json/',
files: [
{
destination: 'tokens.json',
format: 'json'
}
]
}
}
};Creating Token Files
// tokens/global/colors.json
{
"color": {
"blue": {
"50": { "$value": "#EFF6FF", "$type": "color" },
"100": { "$value": "#DBEAFE", "$type": "color" },
"200": { "$value": "#BFDBFE", "$type": "color" },
"300": { "$value": "#93C5FD", "$type": "color" },
"400": { "$value": "#60A5FA", "$type": "color" },
"500": { "$value": "#3B82F6", "$type": "color" },
"600": { "$value": "#2563EB", "$type": "color" },
"700": { "$value": "#1D4ED8", "$type": "color" },
"800": { "$value": "#1E40AF", "$type": "color" },
"900": { "$value": "#1E3A8A", "$type": "color" }
},
"gray": {
"50": { "$value": "#F9FAFB", "$type": "color" },
"100": { "$value": "#F3F4F6", "$type": "color" },
"200": { "$value": "#E5E7EB", "$type": "color" },
"900": { "$value": "#111827", "$type": "color" }
},
"red": {
"500": { "$value": "#EF4444", "$type": "color" }
},
"green": {
"500": { "$value": "#22C55E", "$type": "color" }
}
}
}// tokens/global/spacing.json
{
"spacing": {
"0": { "$value": "0px", "$type": "dimension" },
"1": { "$value": "4px", "$type": "dimension" },
"2": { "$value": "8px", "$type": "dimension" },
"3": { "$value": "12px", "$type": "dimension" },
"4": { "$value": "16px", "$type": "dimension" },
"5": { "$value": "20px", "$type": "dimension" },
"6": { "$value": "24px", "$type": "dimension" },
"8": { "$value": "32px", "$type": "dimension" },
"10": { "$value": "40px", "$type": "dimension" },
"12": { "$value": "48px", "$type": "dimension" },
"16": { "$value": "64px", "$type": "dimension" }
}
}// tokens/alias/color.json
{
"color": {
"primary": {
"default": { "$value": "{color.blue.500}", "$type": "color" },
"hover": { "$value": "{color.blue.600}", "$type": "color" },
"active": { "$value": "{color.blue.700}", "$type": "color" },
"subtle": { "$value": "{color.blue.50}", "$type": "color" }
},
"text": {
"primary": { "$value": "{color.gray.900}", "$type": "color" },
"secondary": { "$value": "{color.gray.500}", "$type": "color" },
"inverse": { "$value": "#FFFFFF", "$type": "color" }
},
"background": {
"primary": { "$value": "#FFFFFF", "$type": "color" },
"secondary": { "$value": "{color.gray.50}", "$type": "color" },
"inverse": { "$value": "{color.gray.900}", "$type": "color" }
},
"border": {
"default": { "$value": "{color.gray.200}", "$type": "color" },
"strong": { "$value": "{color.gray.900}", "$type": "color" }
},
"feedback": {
"error": { "$value": "{color.red.500}", "$type": "color" },
"success": { "$value": "{color.green.500}", "$type": "color" }
}
}
}Building Tokens
npx style-dictionary buildOutput:
/* dist/css/variables.css */
:root {
--color-blue-50: #EFF6FF;
--color-blue-500: #3B82F6;
--color-blue-600: #2563EB;
--color-primary-default: var(--color-blue-500);
--color-primary-hover: var(--color-blue-600);
--color-text-primary: var(--color-gray-900);
--spacing-2: 8px;
--spacing-4: 16px;
--spacing-6: 24px;
}// dist/js/tokens.js
export const colors = {
blue: {
50: '#EFF6FF',
500: '#3B82F6',
600: '#2563EB',
},
primary: {
default: '#3B82F6',
hover: '#2563EB',
},
};
export const spacing = {
2: '8px',
4: '16px',
6: '24px',
};Integrating with Tailwind CSS
// tailwind.config.js
const tokens = require('./dist/json/tokens.json');
module.exports = {
theme: {
colors: {
primary: {
DEFAULT: tokens.color.primary.default,
hover: tokens.color.primary.hover,
subtle: tokens.color.primary.subtle,
},
text: {
primary: tokens.color.text.primary,
secondary: tokens.color.text.secondary,
},
background: {
primary: tokens.color.background.primary,
secondary: tokens.color.background.secondary,
},
},
spacing: {
sm: tokens.spacing[2],
md: tokens.spacing[4],
lg: tokens.spacing[6],
},
},
};Theming with Tokens
// tokens/themes/light.json
{
"color": {
"background": {
"primary": { "$value": "#FFFFFF" },
"secondary": { "$value": "{color.gray.50}" }
},
"text": {
"primary": { "$value": "{color.gray.900}" },
"secondary": { "$value": "{color.gray.500}" }
}
}
}
// tokens/themes/dark.json
{
"color": {
"background": {
"primary": { "$value": "{color.gray.900}" },
"secondary": { "$value": "{color.gray.800}" }
},
"text": {
"primary": { "$value": "#FFFFFF" },
"secondary": { "$value": "{color.gray.400}" }
}
}
}Build CSS with theme support:
/* dist/css/light.css */
[data-theme="light"] {
--color-background-primary: #FFFFFF;
--color-text-primary: var(--color-gray-900);
}
/* dist/css/dark.css */
[data-theme="dark"] {
--color-background-primary: var(--color-gray-900);
--color-text-primary: #FFFFFF;
}Real-World Use Cases
Use Case 1: Multi-Brand Theming
// tokens/brands/acme.json
{
"brand": {
"primary": { "$value": "#FF6B35" },
"secondary": { "$value": "#004E89" },
"font": { "$value": "Inter, sans-serif" }
}
}
// tokens/brands/globex.json
{
"brand": {
"primary": { "$value": "#8B5CF6" },
"secondary": { "$value": "#06B6D4" },
"font": { "$value": "Roboto, sans-serif" }
}
}Build separate token sets for each brand:
// style-dictionary.config.js
['acme', 'globex'].forEach(brand => {
module.exports.platforms.css.files.push({
destination: `${brand}.css`,
format: 'css/variables',
filter: (token) => token.filePath.includes(brand),
});
});Use Case 2: Figma Integration
// scripts/figma-to-tokens.ts
// Sync tokens from Figma to code
interface FigmaVariable {
name: string;
value: string;
type: 'color' | 'float' | 'string';
}
async function syncFigmaTokens(apiKey: string, fileKey: string) {
const response = await fetch(
`https://api.figma.com/v1/files/${fileKey}/variables/local`,
{ headers: { 'X-Figma-Token': apiKey } }
);
const data = await response.json();
const tokens: Record<string, unknown> = {};
for (const variable of data.meta.variables) {
const path = variable.name.split('/').join('.');
const value = variable.valuesByMode[Object.keys(variable.valuesByMode)[0]];
if (variable.resolvedType === 'COLOR') {
setNestedValue(tokens, path, {
$value: `#${Math.round(value.r * 255).toString(16)}${Math.round(value.g * 255).toString(16)}${Math.round(value.b * 255).toString(16)}`,
$type: 'color',
});
} else if (variable.resolvedType === 'FLOAT') {
setNestedValue(tokens, path, {
$value: `${value}px`,
$type: 'dimension',
});
}
}
await Deno.writeTextFile('tokens/figma-sync.json', JSON.stringify(tokens, null, 2));
console.log('Tokens synced from Figma');
}Use Case 3: Runtime Theme Switching
// lib/theme.ts
import lightTokens from '../dist/json/light.json';
import darkTokens from '../dist/json/dark.json';
type Theme = 'light' | 'dark';
const themes: Record<Theme, Record<string, string>> = {
light: flattenTokens(lightTokens),
dark: flattenTokens(darkTokens),
};
function flattenTokens(obj: Record<string, unknown>, prefix = ''): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(obj)) {
const path = prefix ? `${prefix}-${key}` : key;
if (typeof value === 'object' && value !== null && '$value' in value) {
result[path] = (value as { $value: string }).$value;
} else if (typeof value === 'object' && value !== null) {
Object.assign(result, flattenTokens(value as Record<string, unknown>, path));
}
}
return result;
}
export function applyTheme(theme: Theme): void {
const root = document.documentElement;
const tokens = themes[theme];
for (const [key, value] of Object.entries(tokens)) {
root.style.setProperty(`--${key}`, value);
}
root.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}
export function initTheme(): void {
const saved = localStorage.getItem('theme') as Theme | null;
const preferred = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
applyTheme(saved ?? preferred);
}Best Practices for Production
-
Start with global tokens: Define your primitive values first (colors, spacing scale, font sizes). Alias tokens and component tokens should always reference globals.
-
Use semantic naming:
color-background-primaryis better thancolor-white. Semantic names survive design changes; color names don't. -
Document every token: Add
$descriptionto tokens. "Used for primary button backgrounds" is more helpful than just a hex value. -
Version your tokens: Use semantic versioning for token packages. Breaking changes (renaming tokens) require major version bumps.
-
Automate Figma sync: Use the Figma API to pull design tokens directly from design files. This eliminates manual sync errors.
-
Test token output: Write tests that verify generated CSS/JS files contain expected token values. Catch build issues before deployment.
-
Use CSS custom properties for theming: CSS variables enable runtime theme switching without JavaScript bundle changes.
-
Keep tokens platform-agnostic: Don't include platform-specific values in token definitions. Let Style Dictionary transforms handle unit conversions.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
| Hardcoding values instead of using tokens | Inconsistency across components | Lint for hardcoded values with stylelint |
| Too many tokens | Decision fatigue, unused values | Audit usage; remove tokens with zero references |
| Inconsistent naming | Developer confusion | Create a naming convention guide; enforce with linting |
| Not versioning tokens | Breaking changes in production | Use semantic versioning; deprecate before removing |
| Missing dark mode tokens | Incomplete theming | Define both light and dark token sets from the start |
| Token duplication | Maintenance burden | Use references ({color.blue.500}) to avoid duplication |
Performance Optimization
// Build tokens at compile time, not runtime
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "dist/scss/_variables.scss";`,
},
},
},
});
// In production, CSS custom properties are resolved at parse time
// No runtime JavaScript needed for token application// Tree-shake unused tokens in production
// Only include tokens that are actually referenced in your code
// postcss.config.js
module.exports = {
plugins: [
require('postcss-purgecss')({
content: ['./src/**/*.html', './src/**/*.tsx'],
// PurgeCSS will remove unused CSS custom properties
}),
],
};Comparison with Alternatives
| Approach | Design Tokens | CSS-in-JS | Tailwind Config | CSS Variables |
|---|---|---|---|---|
| Multi-platform | Yes (iOS, Android, Web) | No (JS only) | No (CSS only) | No (CSS only) |
| Figma Sync | Native | Manual | Manual | Manual |
| Theming | Built-in | Runtime | Build-time | Runtime |
| Performance | Zero runtime | Runtime overhead | Build-time | Zero runtime |
| Type Safety | Generated types | Native | Generated | Manual |
| Learning Curve | Medium | Low | Low | Low |
Advanced Patterns
Custom Style Dictionary Transforms
// style-dictionary.config.js
StyleDictionary.registerTransform({
name: 'size/pxToRem',
type: 'value',
filter: (token) => token.$type === 'dimension' && token.$value.endsWith('px'),
transformer: (token) => {
const px = parseFloat(token.$value);
return `${px / 16}rem`;
},
});
module.exports = {
platforms: {
css: {
transforms: ['attribute/cti', 'size/pxToRem', 'color/css'],
},
},
};Token Validation
// scripts/validate-tokens.ts
import { z } from 'zod';
const TokenSchema = z.object({
$value: z.string(),
$type: z.enum(['color', 'dimension', 'fontFamily', 'fontWeight', 'duration']),
$description: z.string().optional(),
});
const TokenGroupSchema: z.ZodType = z.lazy(() =>
z.record(z.union([TokenSchema, TokenGroupSchema]))
);
async function validateTokens(path: string) {
const content = JSON.parse(await Deno.readTextFile(path));
const result = TokenGroupSchema.safeParse(content);
if (!result.success) {
console.error(`Invalid tokens in ${path}:`);
console.error(result.error.issues);
return false;
}
return true;
}Testing Strategies
// tests/tokens.test.ts
import { assertEquals, assertExists } from "https://deno.land/std/assert/mod.ts";
const tokens = JSON.parse(await Deno.readTextFile('dist/json/tokens.json'));
Deno.test("primary color is defined", () => {
assertExists(tokens.color.primary.default);
assertEquals(tokens.color.primary.default.startsWith('#'), true);
});
Deno.test("spacing scale is consistent", () => {
const spacingValues = Object.values(tokens.spacing).map(s => parseInt(s as string));
for (let i = 1; i < spacingValues.length; i++) {
assertEquals(spacingValues[i] > spacingValues[i - 1], true);
}
});
Deno.test("all alias tokens resolve", () => {
function checkReferences(obj: Record<string, unknown>) {
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'object' && value !== null) {
if ('$value' in value && typeof (value as any).$value === 'string') {
const ref = (value as any).$value as string;
if (ref.includes('{')) {
// Verify reference resolves
const path = ref.replace(/[{}]/g, '').split('.');
let current: any = tokens;
for (const segment of path) {
assertExists(current[segment], `Reference ${ref} in ${key} does not resolve`);
current = current[segment];
}
}
} else {
checkReferences(value as Record<string, unknown>);
}
}
}
}
checkReferences(tokens);
});Token-Driven Theming and Dark Mode
Design tokens enable theming by defining a set of semantic tokens that reference different primitive values depending on the active theme. The semantic layer abstracts intent from implementation: instead of using color-gray-900 directly, you use color-text-primary that resolves to color-gray-900 in the light theme and color-gray-100 in the dark theme. This separation means component code never changes when themes switch β only the token values change.
:root {
--color-text-primary: var(--color-gray-900);
--color-background: var(--color-white);
--color-surface: var(--color-gray-50);
}
[data-theme="dark"] {
--color-text-primary: var(--color-gray-100);
--color-background: var(--color-gray-950);
--color-surface: var(--color-gray-900);
}Multi-brand theming extends this pattern to support multiple product brands within the same application. Each brand defines its own primitive tokens (brand colors, typography, spacing) while sharing the same semantic layer and component library. The brand configuration can be loaded at runtime, enabling white-label applications where the same codebase serves different brands with completely different visual identities. Store brand token sets as JSON files and load them dynamically based on the tenant or domain.
Token Validation and Governance
As design token systems grow, governance becomes critical to prevent inconsistencies and drift. Automated validation ensures that every token conforms to the defined schema, that color values meet accessibility contrast ratios, and that token names follow the naming convention. Build validation into the CI pipeline so that token changes that violate governance rules are rejected before merging.
Linting tools like Style Dictionary plugins and custom validation scripts can enforce constraints like minimum spacing increments, valid color palette ranges, and typography scale adherence. For example, a validation rule might require that all spacing tokens are multiples of 4px, or that font sizes follow a modular scale. These constraints prevent designers and developers from introducing arbitrary values that break the system's visual consistency.
Token change management requires a review process that considers the impact across all platforms and products. When a token value changes, every component and platform that uses it is affected. Use dependency analysis tools to identify which components and pages reference a specific token before approving changes. Major token changes should follow semantic versioning conventions, with deprecated tokens remaining available for a transition period before removal.
Future Outlook
Design tokens are evolving toward:
- W3C standardization: The Design Tokens Format Module will become a W3C recommendation
- Figma Variables integration: Native two-way sync between Figma and code
- Dynamic theming: Runtime token updates without page reload
- AI-assisted token generation: Automatic token extraction from design mockups
- Cross-framework token sharing: Standardized token packages for React, Vue, Svelte
Conclusion
Design tokens are the foundation of scalable design systems. By abstracting visual decisions into named, platform-agnostic values, they create a single source of truth that keeps design and code in sync across web, iOS, Android, and beyond.
Key takeaways:
- Tokens are atomic design decisions β one token = one visual property
- Three-tier architecture β global β alias β component tokens
- Style Dictionary transforms tokens into any platform format
- Theming is built-in β swap alias token values for instant theme changes
- Figma integration eliminates manual design-to-code sync
Start with your color palette and spacing scale. Define alias tokens for semantic meaning. Build component tokens for specific UI elements. Let Style Dictionary generate the rest. Your design system will thank you.