Design tokens in React transform arbitrary style values into a structured, maintainable system. Instead of color: '#3b82f6' scattered across components, you get color: tokens.color.action.primary with automatic type safety, theme switching, and AI-friendly structure.
This guide covers the complete setup: TypeScript token definitions, consumption patterns, theme switching, and integration with modern React patterns like hooks and context.
Why React Needs Design Tokens
React's component architecture naturally encourages style duplication. Each component gets its own styles, and without central coordination, you end up with:
// Button.tsx
<button style={{ backgroundColor: '#3b82f6' }}>Click</button>
// Link.tsx
<a style={{ color: '#3b82f6' }}>Go</a>
// Badge.tsx
<span style={{ backgroundColor: '#3b81f5' }}>New</span> // typo
Three different files, three style declarations, one accidental typo that breaks consistency. Scaling this to 50 components creates maintenance nightmares.
Design tokens centralize these values:
// Button.tsx
<button style={{ backgroundColor: tokens.color.action.primary }}>Click</button>
// Link.tsx
<a style={{ color: tokens.color.action.primary }}>Go</a>
// Badge.tsx
<span style={{ backgroundColor: tokens.color.action.primary }}>New</span>
One source of truth. Change token value, all components update. TypeScript catches typos. AI can reference tokens by name.
Token Structure for React
Effective React token systems use nested JavaScript objects with TypeScript typing:
// tokens/types.ts
export interface ColorTokens {
text: {
primary: string
secondary: string
tertiary: string
inverse: string
}
surface: {
primary: string
secondary: string
tertiary: string
elevated: string
}
action: {
primary: string
primaryHover: string
primaryActive: string
secondary: string
secondaryHover: string
danger: string
dangerHover: string
}
border: {
default: string
hover: string
focus: string
}
feedback: {
success: string
warning: string
error: string
info: string
}
}
export interface SpacingTokens {
xs: string
sm: string
md: string
lg: string
xl: string
'2xl': string
'3xl': string
}
export interface TypographyTokens {
fontSize: {
xs: string
sm: string
base: string
lg: string
xl: string
'2xl': string
'3xl': string
}
fontWeight: {
normal: number
medium: number
semibold: number
bold: number
}
lineHeight: {
tight: number
normal: number
relaxed: number
}
fontFamily: {
sans: string
mono: string
}
}
export interface EffectTokens {
borderRadius: {
none: string
sm: string
md: string
lg: string
xl: string
full: string
}
boxShadow: {
none: string
sm: string
md: string
lg: string
xl: string
}
}
export interface Tokens {
color: ColorTokens
spacing: SpacingTokens
typography: TypographyTokens
effects: EffectTokens
}
This type structure provides autocomplete and catches invalid token references at compile time.
Implementing Token Values
Create concrete token values for light theme:
// tokens/light.ts
import type { Tokens } from './types'
export const lightTokens: Tokens = {
color: {
text: {
primary: '#111827',
secondary: '#6b7280',
tertiary: '#9ca3af',
inverse: '#ffffff',
},
surface: {
primary: '#ffffff',
secondary: '#f9fafb',
tertiary: '#f3f4f6',
elevated: '#ffffff',
},
action: {
primary: '#3b82f6',
primaryHover: '#2563eb',
primaryActive: '#1d4ed8',
secondary: '#e5e7eb',
secondaryHover: '#d1d5db',
danger: '#ef4444',
dangerHover: '#dc2626',
},
border: {
default: '#e5e7eb',
hover: '#d1d5db',
focus: '#3b82f6',
},
feedback: {
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444',
info: '#3b82f6',
},
},
spacing: {
xs: '0.5rem', // 8px
sm: '0.75rem', // 12px
md: '1rem', // 16px
lg: '1.5rem', // 24px
xl: '2rem', // 32px
'2xl': '3rem', // 48px
'3xl': '4rem', // 64px
},
typography: {
fontSize: {
xs: '0.75rem', // 12px
sm: '0.875rem', // 14px
base: '1rem', // 16px
lg: '1.125rem', // 18px
xl: '1.25rem', // 20px
'2xl': '1.5rem', // 24px
'3xl': '1.875rem', // 30px
},
fontWeight: {
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
},
lineHeight: {
tight: 1.25,
normal: 1.5,
relaxed: 1.75,
},
fontFamily: {
sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
mono: '"SF Mono", Monaco, Consolas, monospace',
},
},
effects: {
borderRadius: {
none: '0',
sm: '0.375rem',
md: '0.5rem',
lg: '0.75rem',
xl: '1rem',
full: '9999px',
},
boxShadow: {
none: 'none',
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1)',
},
},
}
Dark theme follows same structure with different values:
// tokens/dark.ts
import type { Tokens } from './types'
export const darkTokens: Tokens = {
color: {
text: {
primary: '#f9fafb',
secondary: '#d1d5db',
tertiary: '#9ca3af',
inverse: '#111827',
},
surface: {
primary: '#111827',
secondary: '#1f2937',
tertiary: '#374151',
elevated: '#1f2937',
},
action: {
primary: '#3b82f6',
primaryHover: '#2563eb',
primaryActive: '#1d4ed8',
secondary: '#374151',
secondaryHover: '#4b5563',
danger: '#ef4444',
dangerHover: '#dc2626',
},
border: {
default: '#374151',
hover: '#4b5563',
focus: '#3b82f6',
},
feedback: {
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444',
info: '#3b82f6',
},
},
// spacing, typography, effects remain same
spacing: lightTokens.spacing,
typography: lightTokens.typography,
effects: lightTokens.effects,
}
Notice spacing, typography, and effects reference light tokens. These typically don't change between themes—only colors do.
React Context for Token Distribution
Use React Context to provide tokens throughout the component tree:
// context/TokenContext.tsx
import { createContext, useContext, ReactNode } from 'react'
import type { Tokens } from '../tokens/types'
import { lightTokens } from '../tokens/light'
interface TokenContextValue {
tokens: Tokens
}
const TokenContext = createContext<TokenContextValue | undefined>(undefined)
interface TokenProviderProps {
children: ReactNode
tokens?: Tokens
}
export function TokenProvider({ children, tokens = lightTokens }: TokenProviderProps) {
return (
<TokenContext.Provider value={{ tokens }}>
{children}
</TokenContext.Provider>
)
}
export function useTokens(): Tokens {
const context = useContext(TokenContext)
if (!context) {
throw new Error('useTokens must be used within TokenProvider')
}
return context.tokens
}
Wrap your app with TokenProvider:
// App.tsx
import { TokenProvider } from './context/TokenContext'
import { lightTokens } from './tokens/light'
function App() {
return (
<TokenProvider tokens={lightTokens}>
<YourApp />
</TokenProvider>
)
}
Now any component can access tokens via useTokens():
// components/Button.tsx
import { useTokens } from '../context/TokenContext'
export function Button({ children, variant = 'primary' }) {
const tokens = useTokens()
const styles = {
backgroundColor: variant === 'primary'
? tokens.color.action.primary
: tokens.color.action.secondary,
color: variant === 'primary'
? tokens.color.text.inverse
: tokens.color.text.primary,
padding: `${tokens.spacing.sm} ${tokens.spacing.md}`,
borderRadius: tokens.effects.borderRadius.md,
fontSize: tokens.typography.fontSize.base,
fontWeight: tokens.typography.fontWeight.medium,
border: 'none',
cursor: 'pointer',
}
return <button style={styles}>{children}</button>
}
This pattern centralizes token access. Change theme in provider, all components update automatically.
Theme Switching Implementation
Add theme switching by managing which token set the provider uses:
// context/ThemeContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react'
import { TokenProvider } from './TokenContext'
import { lightTokens } from '../tokens/light'
import { darkTokens } from '../tokens/dark'
type Theme = 'light' | 'dark'
interface ThemeContextValue {
theme: Theme
setTheme: (theme: Theme) => void
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined)
interface ThemeProviderProps {
children: ReactNode
defaultTheme?: Theme
}
export function ThemeProvider({ children, defaultTheme = 'light' }: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(defaultTheme)
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
const tokens = theme === 'light' ? lightTokens : darkTokens
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
<TokenProvider tokens={tokens}>
{children}
</TokenProvider>
</ThemeContext.Provider>
)
}
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
Update App to use ThemeProvider:
// App.tsx
import { ThemeProvider } from './context/ThemeContext'
function App() {
return (
<ThemeProvider defaultTheme="light">
<YourApp />
</ThemeProvider>
)
}
Create theme toggle component:
// components/ThemeToggle.tsx
import { useTheme } from '../context/ThemeContext'
import { useTokens } from '../context/TokenContext'
export function ThemeToggle() {
const { theme, toggleTheme } = useTheme()
const tokens = useTokens()
const styles = {
padding: tokens.spacing.sm,
backgroundColor: tokens.color.surface.elevated,
border: `1px solid ${tokens.color.border.default}`,
borderRadius: tokens.effects.borderRadius.md,
cursor: 'pointer',
fontSize: tokens.typography.fontSize.base,
}
return (
<button onClick={toggleTheme} style={styles}>
{theme === 'light' ? '🌙' : '☀️'}
</button>
)
}
Theme switches propagate automatically to all components using useTokens().
Persistent Theme Preference
Save theme preference to localStorage:
// context/ThemeContext.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'
import { TokenProvider } from './TokenContext'
import { lightTokens } from '../tokens/light'
import { darkTokens } from '../tokens/dark'
type Theme = 'light' | 'dark'
interface ThemeContextValue {
theme: Theme
setTheme: (theme: Theme) => void
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined)
function getInitialTheme(): Theme {
if (typeof window === 'undefined') return 'light'
const stored = localStorage.getItem('theme')
if (stored === 'light' || stored === 'dark') return stored
// Respect system preference
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark'
}
return 'light'
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(getInitialTheme)
useEffect(() => {
localStorage.setItem('theme', theme)
}, [theme])
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme)
}
const toggleTheme = () => {
setThemeState(prev => prev === 'light' ? 'dark' : 'light')
}
const tokens = theme === 'light' ? lightTokens : darkTokens
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
<TokenProvider tokens={tokens}>
{children}
</TokenProvider>
</ThemeContext.Provider>
)
}
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
Now theme preference persists across sessions and respects system dark mode preference.
CSS Variables Alternative
For projects that prefer CSS variables over JavaScript:
// tokens/css-variables.ts
import type { Tokens } from './types'
export function tokensToCSS(tokens: Tokens): Record<string, string> {
return {
// Colors - Text
'--color-text-primary': tokens.color.text.primary,
'--color-text-secondary': tokens.color.text.secondary,
'--color-text-tertiary': tokens.color.text.tertiary,
'--color-text-inverse': tokens.color.text.inverse,
// Colors - Surface
'--color-surface-primary': tokens.color.surface.primary,
'--color-surface-secondary': tokens.color.surface.secondary,
'--color-surface-elevated': tokens.color.surface.elevated,
// Colors - Action
'--color-action-primary': tokens.color.action.primary,
'--color-action-primary-hover': tokens.color.action.primaryHover,
'--color-action-secondary': tokens.color.action.secondary,
// Spacing
'--spacing-xs': tokens.spacing.xs,
'--spacing-sm': tokens.spacing.sm,
'--spacing-md': tokens.spacing.md,
'--spacing-lg': tokens.spacing.lg,
'--spacing-xl': tokens.spacing.xl,
// Typography
'--font-size-base': tokens.typography.fontSize.base,
'--font-size-lg': tokens.typography.fontSize.lg,
'--font-weight-medium': String(tokens.typography.fontWeight.medium),
'--line-height-normal': String(tokens.typography.lineHeight.normal),
// Effects
'--border-radius-md': tokens.effects.borderRadius.md,
'--shadow-md': tokens.effects.boxShadow.md,
}
}
Apply CSS variables to document root:
// context/ThemeContext.tsx
import { useEffect } from 'react'
import { tokensToCSS } from '../tokens/css-variables'
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>(getInitialTheme)
const tokens = theme === 'light' ? lightTokens : darkTokens
useEffect(() => {
const cssVars = tokensToCSS(tokens)
Object.entries(cssVars).forEach(([key, value]) => {
document.documentElement.style.setProperty(key, value)
})
}, [tokens])
// ... rest of provider
}
Components reference CSS variables:
// components/Button.tsx
export function Button({ children, variant = 'primary' }) {
const className = variant === 'primary' ? 'btn-primary' : 'btn-secondary'
return <button className={className}>{children}</button>
}
/* Button.css */
.btn-primary {
background-color: var(--color-action-primary);
color: var(--color-text-inverse);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius-md);
}
.btn-primary:hover {
background-color: var(--color-action-primary-hover);
}
CSS variables work well with traditional CSS/SCSS workflows. Choose based on team preference: JavaScript objects for programmatic access, CSS variables for traditional stylesheets.
Styled-Components Integration
For projects using styled-components:
// styled.d.ts
import 'styled-components'
import type { Tokens } from './tokens/types'
declare module 'styled-components' {
export interface DefaultTheme extends Tokens {}
}
// context/ThemeContext.tsx
import { ThemeProvider as StyledThemeProvider } from 'styled-components'
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>(getInitialTheme)
const tokens = theme === 'light' ? lightTokens : darkTokens
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
<StyledThemeProvider theme={tokens}>
{children}
</StyledThemeProvider>
</ThemeContext.Provider>
)
}
Components access tokens via theme prop:
// components/Button.tsx
import styled from 'styled-components'
const StyledButton = styled.button<{ variant?: 'primary' | 'secondary' }>`
background-color: ${props =>
props.variant === 'primary'
? props.theme.color.action.primary
: props.theme.color.action.secondary
};
color: ${props =>
props.variant === 'primary'
? props.theme.color.text.inverse
: props.theme.color.text.primary
};
padding: ${props => props.theme.spacing.sm} ${props => props.theme.spacing.md};
border-radius: ${props => props.theme.effects.borderRadius.md};
font-size: ${props => props.theme.typography.fontSize.base};
border: none;
cursor: pointer;
&:hover {
background-color: ${props =>
props.variant === 'primary'
? props.theme.color.action.primaryHover
: props.theme.color.action.secondaryHover
};
}
`
export function Button({ children, variant = 'primary' }) {
return <StyledButton variant={variant}>{children}</StyledButton>
}
Styled-components provides automatic TypeScript inference and theme switching.
Tailwind CSS Integration
For Tailwind projects, extend config with tokens:
// tailwind.config.js
const { lightTokens } = require('./src/tokens/light')
module.exports = {
theme: {
extend: {
colors: {
'text-primary': lightTokens.color.text.primary,
'text-secondary': lightTokens.color.text.secondary,
'surface-primary': lightTokens.color.surface.primary,
'action-primary': lightTokens.color.action.primary,
},
spacing: {
'xs': lightTokens.spacing.xs,
'sm': lightTokens.spacing.sm,
'md': lightTokens.spacing.md,
'lg': lightTokens.spacing.lg,
},
fontSize: {
'xs': lightTokens.typography.fontSize.xs,
'sm': lightTokens.typography.fontSize.sm,
'base': lightTokens.typography.fontSize.base,
},
},
},
}
Use token-based classes:
export function Button({ children }) {
return (
<button className="bg-action-primary text-white px-md py-sm rounded-md">
{children}
</button>
)
}
For theme switching with Tailwind, use CSS variables:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
'text-primary': 'var(--color-text-primary)',
'action-primary': 'var(--color-action-primary)',
},
},
},
}
ThemeProvider updates CSS variables; Tailwind classes reference them automatically.
Component Token Overrides
Sometimes components need token variations:
// components/Button.tsx
import { useTokens } from '../context/TokenContext'
interface ButtonProps {
children: ReactNode
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
}
export function Button({ children, variant = 'primary', size = 'md' }: ButtonProps) {
const tokens = useTokens()
const variantStyles = {
primary: {
backgroundColor: tokens.color.action.primary,
color: tokens.color.text.inverse,
},
secondary: {
backgroundColor: tokens.color.action.secondary,
color: tokens.color.text.primary,
},
danger: {
backgroundColor: tokens.color.action.danger,
color: tokens.color.text.inverse,
},
}
const sizeStyles = {
sm: {
padding: `${tokens.spacing.xs} ${tokens.spacing.sm}`,
fontSize: tokens.typography.fontSize.sm,
},
md: {
padding: `${tokens.spacing.sm} ${tokens.spacing.md}`,
fontSize: tokens.typography.fontSize.base,
},
lg: {
padding: `${tokens.spacing.md} ${tokens.spacing.lg}`,
fontSize: tokens.typography.fontSize.lg,
},
}
const styles = {
...variantStyles[variant],
...sizeStyles[size],
borderRadius: tokens.effects.borderRadius.md,
fontWeight: tokens.typography.fontWeight.medium,
border: 'none',
cursor: 'pointer',
}
return <button style={styles}>{children}</button>
}
Variants and sizes derive from base tokens, maintaining consistency while offering flexibility.
Testing Components with Tokens
Mock TokenProvider in tests:
// test-utils.tsx
import { render } from '@testing-library/react'
import { TokenProvider } from './context/TokenContext'
import { lightTokens } from './tokens/light'
export function renderWithTokens(ui: React.ReactElement) {
return render(
<TokenProvider tokens={lightTokens}>
{ui}
</TokenProvider>
)
}
Use in tests:
// Button.test.tsx
import { screen } from '@testing-library/react'
import { renderWithTokens } from '../test-utils'
import { Button } from './Button'
test('renders primary button with correct styles', () => {
renderWithTokens(<Button variant="primary">Click</Button>)
const button = screen.getByRole('button')
expect(button).toHaveStyle({
backgroundColor: '#3b82f6',
color: '#ffffff',
})
})
This ensures components work correctly with token context.
Performance Considerations
Token lookups are fast (object property access), but avoid inline object creation:
Inefficient:
function Button() {
const tokens = useTokens()
return (
<button style={{
// New object created on every render
backgroundColor: tokens.color.action.primary,
padding: `${tokens.spacing.sm} ${tokens.spacing.md}`,
}}>
Click
</button>
)
}
Efficient:
function Button() {
const tokens = useTokens()
const styles = useMemo(() => ({
backgroundColor: tokens.color.action.primary,
padding: `${tokens.spacing.sm} ${tokens.spacing.md}`,
}), [tokens])
return <button style={styles}>Click</button>
}
Or use CSS-in-JS libraries (styled-components, Emotion) that handle optimization automatically.
AI Integration
Export tokens in AI-friendly format:
// tokens/ai-docs.ts
import { lightTokens } from './light'
export function generateTokenDocs() {
return `
# Design Tokens Reference
## Colors
- text.primary: ${lightTokens.color.text.primary} - Body text, headings
- text.secondary: ${lightTokens.color.text.secondary} - Supporting text
- action.primary: ${lightTokens.color.action.primary} - Primary buttons, links
- action.primaryHover: ${lightTokens.color.action.primaryHover} - Hover states
- surface.elevated: ${lightTokens.color.surface.elevated} - Cards, modals
## Spacing
- xs: ${lightTokens.spacing.xs} - Tight spacing
- sm: ${lightTokens.spacing.sm} - Button padding
- md: ${lightTokens.spacing.md} - Default spacing
- lg: ${lightTokens.spacing.lg} - Section padding
## Usage Example
\`\`\`tsx
const tokens = useTokens()
<button style={{
backgroundColor: tokens.color.action.primary,
color: tokens.color.text.inverse,
padding: \`\${tokens.spacing.sm} \${tokens.spacing.md}\`,
}}>
Click
</button>
\`\`\`
`.trim()
}
Include this in project README or DESIGN_TOKENS.md for AI code generation tools to reference.
Migration Strategy
Migrating existing React project to design tokens:
- Audit current styles: Find all color/spacing/typography values
- Define token system: Create token structure covering 80% of values
- Set up infrastructure: Implement TokenProvider, useTokens hook
- Migrate incrementally: Convert one component at a time
- Enforce via linting: Add ESLint rules to prevent hardcoded values
ESLint rule to catch hardcoded colors:
// .eslintrc.js
module.exports = {
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Literal[value=/#[0-9a-f]{3,6}/i]',
message: 'Use design tokens instead of hardcoded colors',
},
],
},
}
This catches hex colors and prompts developers to use tokens instead.
Real-World Example
Complete card component using tokens:
// components/Card.tsx
import { ReactNode, CSSProperties } from 'react'
import { useTokens } from '../context/TokenContext'
interface CardProps {
title: string
description: string
image?: string
action?: {
label: string
onClick: () => void
}
children?: ReactNode
}
export function Card({ title, description, image, action, children }: CardProps) {
const tokens = useTokens()
const containerStyle: CSSProperties = {
backgroundColor: tokens.color.surface.elevated,
borderRadius: tokens.effects.borderRadius.lg,
boxShadow: tokens.effects.boxShadow.md,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
}
const imageStyle: CSSProperties = {
width: '100%',
height: '200px',
objectFit: 'cover',
}
const contentStyle: CSSProperties = {
padding: tokens.spacing.lg,
display: 'flex',
flexDirection: 'column',
gap: tokens.spacing.md,
}
const titleStyle: CSSProperties = {
fontSize: tokens.typography.fontSize.xl,
fontWeight: tokens.typography.fontWeight.semibold,
color: tokens.color.text.primary,
margin: 0,
}
const descriptionStyle: CSSProperties = {
fontSize: tokens.typography.fontSize.base,
color: tokens.color.text.secondary,
lineHeight: tokens.typography.lineHeight.normal,
margin: 0,
}
const actionStyle: CSSProperties = {
backgroundColor: tokens.color.action.primary,
color: tokens.color.text.inverse,
padding: `${tokens.spacing.sm} ${tokens.spacing.md}`,
borderRadius: tokens.effects.borderRadius.md,
fontSize: tokens.typography.fontSize.base,
fontWeight: tokens.typography.fontWeight.medium,
border: 'none',
cursor: 'pointer',
marginTop: tokens.spacing.md,
}
return (
<div style={containerStyle}>
{image && <img src={image} alt={title} style={imageStyle} />}
<div style={contentStyle}>
<h3 style={titleStyle}>{title}</h3>
<p style={descriptionStyle}>{description}</p>
{children}
{action && (
<button style={actionStyle} onClick={action.onClick}>
{action.label}
</button>
)}
</div>
</div>
)
}
This card component is fully token-driven. Change theme, it updates automatically. No hardcoded values anywhere.
Tools and Libraries
FramingUI: Pre-built React token systems with TypeScript definitions, theme switching, and AI-optimized structure.
Vanilla Extract: Zero-runtime CSS-in-JS with TypeScript token support.
Stitches: Modern CSS-in-JS with built-in theming and variants.
Theme UI: Library specifically for theme-driven development.
Choose based on project needs, but all integrate with the token patterns described here.
Conclusion
React design tokens turn ad-hoc styling into a maintainable system. The upfront setup—TypeScript types, context providers, theme switching—pays off immediately when building the second component.
For small projects, start with light/dark themes and core color/spacing tokens. For larger projects, add component-specific tokens, multiple themes, and CSS variable integration.
The goal isn't perfect abstraction—it's consistent, maintainable UI that scales. Design tokens provide that foundation. React's context system distributes them effectively. And TypeScript ensures correctness at compile time.
Start with the token structure that fits your project. Implement TokenProvider and useTokens hook. Build one component using tokens. Then generate the rest with AI, knowing consistency is built in from the start.