MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr πŸ”₯ tagline

Hey there πŸ‘‹ I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 β€” present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 β€” Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms β€” earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Design Tokens: Systematic UI Design

Implement design tokens: naming conventions, tooling, multi-platform export, and theming.

Design TokensUIDesign SystemsFrontend

By MinhVo

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.

Design system architecture

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.

Token architecture layers

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 values
  • spacing β€” Margin, padding, gap values
  • typography β€” Font family, size, weight, line height
  • border β€” Border width, radius, style
  • shadow β€” Box shadow values
  • opacity β€” Transparency values
  • zIndex β€” Stacking order
  • motion β€” 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-dictionary

Create 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 build

Output:

/* 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

  1. Start with global tokens: Define your primitive values first (colors, spacing scale, font sizes). Alias tokens and component tokens should always reference globals.

  2. Use semantic naming: color-background-primary is better than color-white. Semantic names survive design changes; color names don't.

  3. Document every token: Add $description to tokens. "Used for primary button backgrounds" is more helpful than just a hex value.

  4. Version your tokens: Use semantic versioning for token packages. Breaking changes (renaming tokens) require major version bumps.

  5. Automate Figma sync: Use the Figma API to pull design tokens directly from design files. This eliminates manual sync errors.

  6. Test token output: Write tests that verify generated CSS/JS files contain expected token values. Catch build issues before deployment.

  7. Use CSS custom properties for theming: CSS variables enable runtime theme switching without JavaScript bundle changes.

  8. Keep tokens platform-agnostic: Don't include platform-specific values in token definitions. Let Style Dictionary transforms handle unit conversions.

Common Pitfalls and Solutions

PitfallImpactSolution
Hardcoding values instead of using tokensInconsistency across componentsLint for hardcoded values with stylelint
Too many tokensDecision fatigue, unused valuesAudit usage; remove tokens with zero references
Inconsistent namingDeveloper confusionCreate a naming convention guide; enforce with linting
Not versioning tokensBreaking changes in productionUse semantic versioning; deprecate before removing
Missing dark mode tokensIncomplete themingDefine both light and dark token sets from the start
Token duplicationMaintenance burdenUse 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

ApproachDesign TokensCSS-in-JSTailwind ConfigCSS Variables
Multi-platformYes (iOS, Android, Web)No (JS only)No (CSS only)No (CSS only)
Figma SyncNativeManualManualManual
ThemingBuilt-inRuntimeBuild-timeRuntime
PerformanceZero runtimeRuntime overheadBuild-timeZero runtime
Type SafetyGenerated typesNativeGeneratedManual
Learning CurveMediumLowLowLow

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:

  1. Tokens are atomic design decisions β€” one token = one visual property
  2. Three-tier architecture β€” global β†’ alias β†’ component tokens
  3. Style Dictionary transforms tokens into any platform format
  4. Theming is built-in β€” swap alias token values for instant theme changes
  5. 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.