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?
- Easy to read - Hue is the color, saturation is vividness, lightness is brightness
- Easy to adjust - Darken by reducing lightness, desaturate by reducing saturation
- 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
- Components - How components use tokens
- Creating Themes - Define your own tokens