Tutorial

React Design Tokens Setup: Complete Implementation Guide

Implement design tokens in React. TypeScript setup, theme switching, and AI-friendly token architecture.

FramingUI Team15 min read

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:

  1. Audit current styles: Find all color/spacing/typography values
  2. Define token system: Create token structure covering 80% of values
  3. Set up infrastructure: Implement TokenProvider, useTokens hook
  4. Migrate incrementally: Convert one component at a time
  5. 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.

Ready to build with FramingUI?

Build consistent UI with AI-ready design tokens. No more hallucinated colors or spacing.

Try FramingUI
Share

Related Posts