CSS Design Tokens
Token Structure
/* tokens.css */
:root {
/* Colors - Semantic */
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-secondary: #64748b;
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-error: #ef4444;
/* Colors - Neutral */
--color-background: #ffffff;
--color-surface: #f8fafc;
--color-border: #e2e8f0;
--color-text: #1e293b;
--color-text-muted: #64748b;
/* Spacing Scale (4px base) */
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-10: 2.5rem; /* 40px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
/* Typography */
--font-sans: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--text-3xl: 1.875rem; /* 30px */
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.75;
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
/* Borders */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
/* Transitions */
--duration-fast: 150ms;
--duration-normal: 250ms;
--duration-slow: 350ms;
--ease-default: cubic-bezier(0.4, 0, 0.2, 1);
}
Dark Mode
[data-theme="dark"] {
--color-primary: #3b82f6;
--color-primary-hover: #60a5fa;
--color-background: #0f172a;
--color-surface: #1e293b;
--color-border: #334155;
--color-text: #f1f5f9;
--color-text-muted: #94a3b8;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
}
Usage in Components
// Button.tsx
const buttonStyles: CSSProperties = {
backgroundColor: 'var(--color-primary)',
color: 'var(--color-background)',
padding: 'var(--space-2) var(--space-4)',
borderRadius: 'var(--radius-md)',
fontSize: 'var(--text-sm)',
fontWeight: 'var(--font-medium)',
transition: `background-color var(--duration-fast) var(--ease-default)`,
};
/* Button.css */
.btn {
background-color: var(--color-primary);
color: var(--color-background);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-md);
font-size: var(--text-sm);
font-weight: var(--font-medium);
transition: background-color var(--duration-fast) var(--ease-default);
}
.btn:hover {
background-color: var(--color-primary-hover);
}
Token Naming Convention
--{category}-{property}-{variant}
Examples:
--color-primary
--color-primary-hover
--space-4
--text-lg
--radius-md
--shadow-lg
Component Token Mapping
| Component |
Tokens |
| Button |
--color-primary, --space-2/4, --radius-md, --text-sm |
| Input |
--color-border, --space-2/3, --radius-md, --text-base |
| Card |
--color-surface, --space-4, --radius-lg, --shadow-md |
| Badge |
--color-*, --space-1/2, --radius-full, --text-xs |
Theme Provider Pattern
// ThemeProvider.tsx
type Theme = 'light' | 'dark' | 'system';
export const ThemeProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<Theme>('system');
useEffect(() => {
const root = document.documentElement;
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const resolved = theme === 'system' ? (systemDark ? 'dark' : 'light') : theme;
root.setAttribute('data-theme', resolved);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
Best Practices
- Use semantic names (
--color-primary) over literal (--color-blue)
- Define scales with consistent ratios (4px spacing, modular type)
- Keep token count minimal but comprehensive
- Document token purpose in comments
- Test all tokens in both light/dark modes