Design Tokens

Design tokens are the foundational values that define a theme's visual language. They provide a consistent vocabulary for colors, spacing, typography, and other design decisions.

What Are Design Tokens?

Tokens are named values that represent design decisions:

const tokens = {
  colors: {
    primary: 'hsl(222, 47%, 11%)',      // Brand color
    background: 'hsl(0, 0%, 100%)',      // Page background
    foreground: 'hsl(222, 84%, 5%)',     // Text color
  },
  spacing: {
    sm: '0.5rem',                        // 8px
    md: '1rem',                          // 16px
  },
  borderRadius: {
    md: '0.5rem',                        // 8px
  },
};

Instead of hardcoding #1e293b everywhere, you use primary. This enables:

  • Consistency - Same color everywhere it's used
  • Theming - Change one value, update entire UI
  • Color Modes - Easy light/dark switching

Token Categories

Colors

The core color palette for your theme.

Token Purpose Example
primary Brand/action color Buttons, links
primaryForeground Text on primary Button text
secondary Secondary actions Secondary buttons
secondaryForeground Text on secondary Button text
background Page background Body background
foreground Primary text Body text
card Card background Card components
cardForeground Card text Card content
muted Muted backgrounds Disabled states
mutedForeground Muted text Help text, placeholders
accent Accent highlights Hover states
accentForeground Text on accent Accent content
destructive Error/danger Delete buttons, errors
destructiveForeground Text on destructive Error button text
border Border color Card borders, dividers
input Input borders Form field borders
ring Focus ring Focus indicators

Spacing

Consistent spacing scale for margins, padding, and gaps.

Token Value Use Case
xs 0.25rem (4px) Tight spacing, icons
sm 0.5rem (8px) Small gaps, inline spacing
md 1rem (16px) Default padding, margins
lg 1.5rem (24px) Section spacing
xl 2rem (32px) Large gaps
2xl 3rem (48px) Major sections

Border Radius

Rounded corner values.

Token Value Use Case
sm 0.25rem Subtle rounding
md 0.5rem Buttons, inputs
lg 1rem Cards, modals
full 9999px Pills, avatars

Font Family

Typography stacks.

Token Value
sans System UI stack for body text
mono Monospace stack for code

Font Size

Type scale for text sizing.

Token Value Use Case
xs 0.75rem (12px) Captions, labels
sm 0.875rem (14px) Small text, buttons
base 1rem (16px) Body text
lg 1.125rem (18px) Lead text
xl 1.25rem (20px) Subheadings
2xl 1.5rem (24px) Headings
3xl 1.875rem (30px) Large headings

Shadows

Box shadow values for elevation.

Token Use Case
sm Subtle lift (buttons)
md Cards, dropdowns
lg Modals, popovers

Token Interface

The TypeScript interface for tokens:

interface DesignTokens {
  colors: {
    primary: string;
    primaryForeground: string;
    secondary: string;
    secondaryForeground: string;
    destructive: string;
    destructiveForeground: string;
    muted: string;
    mutedForeground: string;
    accent: string;
    accentForeground: string;
    background: string;
    foreground: string;
    card: string;
    cardForeground: string;
    border: string;
    input: string;
    ring: string;
  };
  spacing: {
    xs: string;
    sm: string;
    md: string;
    lg: string;
    xl: string;
    '2xl': string;
  };
  borderRadius: {
    sm: string;
    md: string;
    lg: string;
    full: string;
  };
  fontFamily: {
    sans: string;
    mono: string;
  };
  fontSize: {
    xs: string;
    sm: string;
    base: string;
    lg: string;
    xl: string;
    '2xl': string;
    '3xl': string;
  };
  shadows: {
    sm: string;
    md: string;
    lg: string;
  };
}

How Tokens Become CSS

When a theme is applied, tokens are injected as CSS custom properties:

// Token definition
colors: {
  primary: 'hsl(222, 47%, 11%)',
  background: 'hsl(0, 0%, 100%)',
}

// Becomes CSS variables
:root {
  --primary: 222 47% 11%;
  --background: 0 0% 100%;
  --theme-primary: hsl(222, 47%, 11%);
  --theme-background: hsl(0, 0%, 100%);
}

Two formats are set:

  • --primary (shadcn format) - Space-separated HSL values for Tailwind
  • --theme-primary (full HSL) - Complete HSL for custom CSS

Using Tokens in CSS

Tailwind Classes

Use the standard Tailwind color classes - they're mapped to CSS variables:

<div class="bg-background text-foreground">
  <button class="bg-primary text-primary-foreground">
    Click me
  </button>
</div>

Custom CSS

Reference the --theme-* variables directly:

.my-component {
  background: var(--theme-background);
  color: var(--theme-foreground);
  border: 1px solid var(--theme-border);
}

Using Tokens in JavaScript

useDesignTokens Hook

import { useDesignTokens } from '@/hooks/use-theme';

function MyComponent() {
  const tokens = useDesignTokens();

  return (
    <div style={{
      backgroundColor: tokens.colors.background,
      padding: tokens.spacing.md,
    }}>
      Content
    </div>
  );
}

useToken Hook

Get a specific token value:

import { useToken } from '@/hooks/use-theme';

function MyComponent() {
  const primaryColor = useToken('colors', 'primary');
  const spacing = useToken('spacing', 'md');

  return <div style={{ color: primaryColor, padding: spacing }} />;
}

Color Format: HSL

All colors use HSL (Hue, Saturation, Lightness) format:

hsl(222, 47%, 11%)
    │    │    │
    │    │    └── Lightness (0-100%)
    │    └─────── Saturation (0-100%)
    └──────────── Hue (0-360°)

Why HSL?

  1. Easy to read - Hue is the color, saturation is vividness, lightness is brightness
  2. Easy to adjust - Darken by reducing lightness, desaturate by reducing saturation
  3. Predictable - Related colors share the same hue

Creating Color Variations

const primary = 'hsl(222, 47%, 11%)';       // Base
const primaryLight = 'hsl(222, 47%, 20%)';  // Lighter (increase L)
const primaryDark = 'hsl(222, 47%, 5%)';    // Darker (decrease L)
const primaryMuted = 'hsl(222, 20%, 11%)';  // Muted (decrease S)

Example Token Sets

Light Mode Tokens

export const lightTokens: DesignTokens = {
  colors: {
    primary: 'hsl(222, 47%, 11%)',
    primaryForeground: 'hsl(210, 40%, 98%)',
    secondary: 'hsl(210, 40%, 96%)',
    secondaryForeground: 'hsl(222, 47%, 11%)',
    background: 'hsl(0, 0%, 100%)',
    foreground: 'hsl(222, 84%, 5%)',
    card: 'hsl(0, 0%, 100%)',
    cardForeground: 'hsl(222, 84%, 5%)',
    muted: 'hsl(210, 40%, 96%)',
    mutedForeground: 'hsl(215, 16%, 47%)',
    accent: 'hsl(210, 40%, 96%)',
    accentForeground: 'hsl(222, 47%, 11%)',
    destructive: 'hsl(0, 84%, 60%)',
    destructiveForeground: 'hsl(210, 40%, 98%)',
    border: 'hsl(214, 32%, 91%)',
    input: 'hsl(214, 32%, 91%)',
    ring: 'hsl(222, 84%, 5%)',
  },
  // ...spacing, borderRadius, etc.
};

Dark Mode Tokens

export const darkTokens: DesignTokens = {
  colors: {
    primary: 'hsl(210, 40%, 98%)',
    primaryForeground: 'hsl(222, 47%, 11%)',
    secondary: 'hsl(217, 33%, 17%)',
    secondaryForeground: 'hsl(210, 40%, 98%)',
    background: 'hsl(222, 84%, 5%)',
    foreground: 'hsl(210, 40%, 98%)',
    card: 'hsl(222, 84%, 5%)',
    cardForeground: 'hsl(210, 40%, 98%)',
    muted: 'hsl(217, 33%, 17%)',
    mutedForeground: 'hsl(215, 20%, 65%)',
    accent: 'hsl(217, 33%, 17%)',
    accentForeground: 'hsl(210, 40%, 98%)',
    destructive: 'hsl(0, 63%, 31%)',
    destructiveForeground: 'hsl(210, 40%, 98%)',
    border: 'hsl(217, 33%, 17%)',
    input: 'hsl(217, 33%, 17%)',
    ring: 'hsl(213, 27%, 84%)',
  },
  // ...spacing, borderRadius, etc.
};

Monochrome Tokens

export const monochromeTokens: DesignTokens = {
  colors: {
    primary: 'hsl(0, 0%, 15%)',           // No saturation
    primaryForeground: 'hsl(0, 0%, 100%)',
    secondary: 'hsl(0, 0%, 96%)',
    secondaryForeground: 'hsl(0, 0%, 15%)',
    background: 'hsl(0, 0%, 100%)',
    foreground: 'hsl(0, 0%, 9%)',
    card: 'hsl(0, 0%, 100%)',
    cardForeground: 'hsl(0, 0%, 9%)',
    muted: 'hsl(0, 0%, 96%)',
    mutedForeground: 'hsl(0, 0%, 45%)',
    accent: 'hsl(0, 0%, 92%)',
    accentForeground: 'hsl(0, 0%, 15%)',
    destructive: 'hsl(0, 0%, 25%)',       // Gray instead of red
    destructiveForeground: 'hsl(0, 0%, 100%)',
    border: 'hsl(0, 0%, 90%)',
    input: 'hsl(0, 0%, 90%)',
    ring: 'hsl(0, 0%, 15%)',
  },
  // ...
};

Best Practices

1. Use Semantic Names

❌ Don't use literal color names:

colors: { blue: '...', darkBlue: '...' }

✅ Use purpose-based names:

colors: { primary: '...', primaryForeground: '...' }

2. Maintain Contrast

Ensure foreground/background pairs meet WCAG standards:

// Good - high contrast
background: 'hsl(0, 0%, 100%)',    // White
foreground: 'hsl(222, 84%, 5%)',   // Near black

// Bad - low contrast
background: 'hsl(0, 0%, 90%)',     // Light gray
foreground: 'hsl(0, 0%, 70%)',     // Medium gray

3. Test All Modes

Always define and test tokens for:

  • Light mode
  • Dark mode
  • Monochrome mode

4. Keep Spacing Consistent

Use the spacing scale, don't create arbitrary values:

// Good
padding: tokens.spacing.md  // 1rem

// Bad
padding: '17px'  // Arbitrary

Next Steps