Design Tokens: Bridging Design and Development
Learn how to define, structure, and distribute design tokens in a TypeScript design system, creating a single source of truth for colors, spacing, typography, and more.
Design tokens are the atomic values of a design system — the colors, spacing, typography, shadows, and border radii that define your visual language. They are the bridge between what designers create in Figma and what engineers implement in code. Without tokens, design decisions are scattered across stylesheets, component files, and design tools with no shared vocabulary. With tokens, a single change to your primary color propagates from design specs through your component library to every product that consumes it. This article covers everything from token taxonomy to build-time transformation and runtime theming.
What Design Tokens Are (and Are Not)
A design token is a named, platform-agnostic value that represents a design decision. The key word is "decision." A hex code like #2563EB is a value. Naming it color.brand.primary makes it a decision — you have decided that this shade of blue represents your brand's primary action color.
Tokens operate at multiple levels of abstraction:
// Level 1: Global tokens — raw values with descriptive names
const globalTokens = {
colors: {
blue50: '#EFF6FF',
blue100: '#DBEAFE',
blue500: '#3B82F6',
blue600: '#2563EB',
blue700: '#1D4ED8',
gray50: '#F9FAFB',
gray100: '#F3F4F6',
gray500: '#6B7280',
gray900: '#111827',
red500: '#EF4444',
red600: '#DC2626',
green500: '#22C55E',
green600: '#16A34A',
},
spacing: {
'0': '0px',
'1': '4px',
'2': '8px',
'3': '12px',
'4': '16px',
'5': '20px',
'6': '24px',
'8': '32px',
'10': '40px',
'12': '48px',
'16': '64px',
},
} as const;
// Level 2: Semantic tokens — intent-based aliases
const semanticTokens = {
colors: {
'bg.primary': globalTokens.colors.blue600,
'bg.secondary': globalTokens.colors.gray100,
'bg.danger': globalTokens.colors.red600,
'bg.success': globalTokens.colors.green600,
'text.primary': globalTokens.colors.gray900,
'text.secondary': globalTokens.colors.gray500,
'text.onPrimary': '#FFFFFF',
'border.default': globalTokens.colors.gray100,
'border.focus': globalTokens.colors.blue500,
},
spacing: {
'spacing.component.gap': globalTokens.spacing['3'],
'spacing.component.padding': globalTokens.spacing['4'],
'spacing.section.gap': globalTokens.spacing['8'],
'spacing.page.padding': globalTokens.spacing['6'],
},
} as const;
// Level 3: Component tokens — component-specific assignments
const componentTokens = {
button: {
primary: {
bg: semanticTokens.colors['bg.primary'],
text: semanticTokens.colors['text.onPrimary'],
hoverBg: globalTokens.colors.blue700,
padding: `${globalTokens.spacing['2']} ${globalTokens.spacing['4']}`,
borderRadius: '6px',
},
secondary: {
bg: 'transparent',
text: semanticTokens.colors['text.primary'],
hoverBg: globalTokens.colors.gray50,
padding: `${globalTokens.spacing['2']} ${globalTokens.spacing['4']}`,
borderRadius: '6px',
},
},
} as const;This three-tier architecture is fundamental. Global tokens are the raw palette — they rarely change and provide the full range of available values. Semantic tokens assign meaning — bg.primary can be swapped for theming without touching component code. Component tokens bind semantic decisions to specific component states.
Structuring Tokens in TypeScript
The as const assertion is the single most important TypeScript feature for design tokens. It preserves literal types, which means TypeScript knows the exact values in your token set and can enforce them at compile time.
Build your token package as a standalone module that other packages depend on:
// packages/tokens/src/colors.ts
export const colors = {
// Brand
brand: {
50: '#EFF6FF',
100: '#DBEAFE',
200: '#BFDBFE',
300: '#93C5FD',
400: '#60A5FA',
500: '#3B82F6',
600: '#2563EB',
700: '#1D4ED8',
800: '#1E40AF',
900: '#1E3A8A',
},
// Neutrals
neutral: {
0: '#FFFFFF',
50: '#F9FAFB',
100: '#F3F4F6',
200: '#E5E7EB',
300: '#D1D5DB',
400: '#9CA3AF',
500: '#6B7280',
600: '#4B5563',
700: '#374151',
800: '#1F2937',
900: '#111827',
},
// Feedback
success: { 500: '#22C55E', 600: '#16A34A', 50: '#F0FDF4' },
warning: { 500: '#F59E0B', 600: '#D97706', 50: '#FFFBEB' },
error: { 500: '#EF4444', 600: '#DC2626', 50: '#FEF2F2' },
info: { 500: '#3B82F6', 600: '#2563EB', 50: '#EFF6FF' },
} as const;// packages/tokens/src/typography.ts
export const typography = {
fontFamilies: {
sans: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
mono: "'JetBrains Mono', 'Fira Code', monospace",
},
fontSizes: {
xs: '0.75rem', // 12px
sm: '0.875rem', // 14px
md: '1rem', // 16px
lg: '1.125rem', // 18px
xl: '1.25rem', // 20px
'2xl': '1.5rem', // 24px
'3xl': '1.875rem', // 30px
'4xl': '2.25rem', // 36px
},
fontWeights: {
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
},
lineHeights: {
tight: 1.25,
normal: 1.5,
relaxed: 1.75,
},
} as const;// packages/tokens/src/index.ts
export { colors } from './colors';
export { typography } from './typography';
export { spacing } from './spacing';
export { shadows } from './shadows';
export { radii } from './radii';
// Re-export utility types
export type ColorPath = /* derived type */;
export type SpacingToken = keyof typeof import('./spacing').spacing;With these types, you can create utility types that constrain component props to valid token values:
import { colors, spacing, typography } from '@messier/tokens';
// Utility type to extract deep keys as dot-notation paths
type DeepKeys<T, Prefix extends string = ''> = T extends object
? {
[K in keyof T & string]: T[K] extends object
? DeepKeys<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[keyof T & string]
: never;
type ColorToken = DeepKeys<typeof colors>; // "brand.50" | "brand.100" | ... | "neutral.0" | ...
type SpacingToken = keyof typeof spacing; // "0" | "1" | "2" | ...Transforming Tokens for Multiple Platforms
Tokens defined in TypeScript are immediately usable in your React component library. But design systems often serve multiple platforms — web apps, React Native, email templates, documentation sites. The W3C Design Token Community Group format provides a platform-agnostic way to define tokens that can be transformed into any output format.
You can author in TypeScript and generate the W3C format, or author in JSON and generate TypeScript. Either direction works. Here is a build script that transforms TypeScript tokens into CSS custom properties and Tailwind config:
// scripts/build-tokens.ts
import fs from 'fs';
import path from 'path';
import { colors, spacing, typography, shadows, radii } from '../src';
function flattenObject(obj: Record<string, any>, prefix = ''): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(obj)) {
const newKey = prefix ? `${prefix}-${key}` : key;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
Object.assign(result, flattenObject(value, newKey));
} else {
result[newKey] = String(value);
}
}
return result;
}
// Generate CSS custom properties
function generateCSS(): string {
const lines = [':root {'];
const flatColors = flattenObject(colors, 'color');
for (const [key, value] of Object.entries(flatColors)) {
lines.push(` --${key}: ${value};`);
}
const flatSpacing = flattenObject(spacing, 'spacing');
for (const [key, value] of Object.entries(flatSpacing)) {
lines.push(` --${key}: ${value};`);
}
const flatTypography = flattenObject(typography, 'font');
for (const [key, value] of Object.entries(flatTypography)) {
lines.push(` --${key}: ${value};`);
}
lines.push('}');
return lines.join('\n');
}
// Generate Tailwind config extension
function generateTailwindConfig(): string {
return `// Auto-generated — do not edit manually
module.exports = {
theme: {
extend: {
colors: ${JSON.stringify(colors, null, 6)},
spacing: ${JSON.stringify(spacing, null, 6)},
fontFamily: ${JSON.stringify(typography.fontFamilies, null, 6)},
fontSize: ${JSON.stringify(typography.fontSizes, null, 6)},
fontWeight: ${JSON.stringify(typography.fontWeights, null, 6)},
},
},
};`;
}
// Write outputs
const outDir = path.resolve(__dirname, '../dist');
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.join(outDir, 'tokens.css'), generateCSS());
fs.writeFileSync(path.join(outDir, 'tailwind.config.js'), generateTailwindConfig());
console.log('Tokens built successfully.');This single source of truth in TypeScript generates CSS, Tailwind, and (with minor additions) SCSS, JSON, or any other format your consumers need.
Using Tokens at Runtime for Theming
Static tokens work for single-brand applications, but many products need runtime theming — dark mode, high-contrast mode, or white-label customization. CSS custom properties are the mechanism that makes this possible.
Combine your TypeScript tokens with a theme provider that swaps CSS variables at runtime:
import React, { createContext, useContext, useEffect, useState } from 'react';
interface Theme {
name: string;
tokens: Record<string, string>;
}
const lightTheme: Theme = {
name: 'light',
tokens: {
'--color-bg-primary': '#FFFFFF',
'--color-bg-secondary': '#F9FAFB',
'--color-text-primary': '#111827',
'--color-text-secondary': '#6B7280',
'--color-border': '#E5E7EB',
'--color-accent': '#2563EB',
'--shadow-sm': '0 1px 2px rgba(0, 0, 0, 0.05)',
'--shadow-md': '0 4px 6px rgba(0, 0, 0, 0.07)',
},
};
const darkTheme: Theme = {
name: 'dark',
tokens: {
'--color-bg-primary': '#111827',
'--color-bg-secondary': '#1F2937',
'--color-text-primary': '#F9FAFB',
'--color-text-secondary': '#9CA3AF',
'--color-border': '#374151',
'--color-accent': '#60A5FA',
'--shadow-sm': '0 1px 2px rgba(0, 0, 0, 0.3)',
'--shadow-md': '0 4px 6px rgba(0, 0, 0, 0.4)',
},
};
interface ThemeContextValue {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>(lightTheme);
useEffect(() => {
const root = document.documentElement;
for (const [property, value] of Object.entries(theme.tokens)) {
root.style.setProperty(property, value);
}
}, [theme]);
const toggleTheme = () => {
setTheme((current) => (current.name === 'light' ? darkTheme : lightTheme));
};
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme(): ThemeContextValue {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}Components reference CSS variables instead of hard-coded token values, making them automatically theme-aware:
.button-primary {
background-color: var(--color-accent);
color: var(--color-bg-primary);
box-shadow: var(--shadow-sm);
}
.card {
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-md);
}Governance and Token Lifecycle
Tokens are not static. Brands evolve, accessibility audits reveal contrast issues, and new product lines demand additional values. A clear lifecycle process prevents token sprawl.
Every token addition should answer three questions: What decision does it represent? Where will it be used? Does an existing token already serve this purpose?
Deprecation should be gradual. Mark tokens as deprecated in your TypeScript source using JSDoc and a utility type:
interface DeprecatedToken {
value: string;
/** @deprecated Use `bg.primary` instead. Will be removed in v3.0 */
deprecatedMessage: string;
replacement: string;
}
// In practice, use JSDoc on the token itself
export const semanticColors = {
/** @deprecated Use `bg.primary` instead */
'background.brand': '#2563EB',
'bg.primary': '#2563EB',
};Run a lint rule that flags usage of deprecated tokens in consuming codebases. Provide a codemod that automates the migration. Only remove the token after a full major version cycle.
Conclusion
Design tokens are the connective tissue of a design system. Defined in TypeScript with as const, they provide compile-time safety. Structured in three tiers — global, semantic, component — they balance flexibility with clarity. Transformed at build time, they serve any platform. Applied through CSS custom properties, they enable runtime theming. And governed with a clear lifecycle, they remain trustworthy as your system evolves. Get your tokens right, and the rest of the design system falls into place.
Related Articles
Versioning and Publishing Component Libraries
A practical guide to versioning, releasing, and distributing TypeScript component libraries using semantic versioning, changesets, and automated CI/CD publishing pipelines.
Measuring the ROI of a Design System
A practical framework for measuring the return on investment of a design system, covering efficiency metrics, quality improvements, consistency gains, and how to communicate value to stakeholders.
Documentation That Developers Actually Read
How to create design system documentation that developers engage with, covering information architecture, interactive examples, API references, and documentation-as-code workflows.