Building a Design System: Components and Tokens

19 min read3756 words

After building design systems for teams ranging from 5 to 500+ developers, I've learned that the secret to a successful design system isn't just good components—it's a foundation built on solid design tokens and clear implementation strategies that teams actually want to use.

The biggest mistake I see teams make is starting with components instead of tokens. When I built my first design system, I spent months perfecting buttons and forms, only to realize later that inconsistent colors, spacing, and typography made everything look disjointed. Starting with tokens changed everything.

Here's everything I've learned about building design systems that scale, from the foundational tokens to production-ready components that teams love using.

The Foundation: Design Tokens

Design tokens are the atomic values of your design system—the colors, spacing, typography, and other design decisions that form the foundation of every component.

Understanding Token Categories

// tokens/foundation.json
{
  "color": {
    "primitive": {
      "red": {
        "50": { "value": "#fef2f2" },
        "100": { "value": "#fee2e2" },
        "500": { "value": "#ef4444" },
        "900": { "value": "#7f1d1d" }
      },
      "blue": {
        "50": { "value": "#eff6ff" },
        "500": { "value": "#3b82f6" },
        "900": { "value": "#1e3a8a" }
      }
    },
    "semantic": {
      "primary": { "value": "{color.primitive.blue.500}" },
      "danger": { "value": "{color.primitive.red.500}" },
      "success": { "value": "{color.primitive.green.500}" }
    },
    "component": {
      "button": {
        "primary": {
          "background": { "value": "{color.semantic.primary}" },
          "text": { "value": "{color.primitive.white}" },
          "border": { "value": "transparent" }
        },
        "secondary": {
          "background": { "value": "transparent" },
          "text": { "value": "{color.semantic.primary}" },
          "border": { "value": "{color.semantic.primary}" }
        }
      }
    }
  },
  "spacing": {
    "primitive": {
      "0": { "value": "0px" },
      "1": { "value": "4px" },
      "2": { "value": "8px" },
      "3": { "value": "12px" },
      "4": { "value": "16px" },
      "6": { "value": "24px" },
      "8": { "value": "32px" },
      "12": { "value": "48px" },
      "16": { "value": "64px" }
    },
    "semantic": {
      "padding": {
        "small": { "value": "{spacing.primitive.2}" },
        "medium": { "value": "{spacing.primitive.4}" },
        "large": { "value": "{spacing.primitive.6}" }
      },
      "gap": {
        "small": { "value": "{spacing.primitive.2}" },
        "medium": { "value": "{spacing.primitive.4}" },
        "large": { "value": "{spacing.primitive.6}" }
      }
    }
  },
  "typography": {
    "fontFamily": {
      "sans": { "value": "Inter, system-ui, sans-serif" },
      "serif": { "value": "Playfair Display, serif" },
      "mono": { "value": "Fira Code, monospace" }
    },
    "fontSize": {
      "xs": { "value": "12px" },
      "sm": { "value": "14px" },
      "base": { "value": "16px" },
      "lg": { "value": "18px" },
      "xl": { "value": "20px" },
      "2xl": { "value": "24px" },
      "3xl": { "value": "30px" },
      "4xl": { "value": "36px" }
    },
    "lineHeight": {
      "tight": { "value": "1.25" },
      "normal": { "value": "1.5" },
      "loose": { "value": "1.75" }
    }
  }
}

Token Processing and Generation

// build/tokens.js
const StyleDictionary = require('style-dictionary');
 
// Custom transform to handle references
StyleDictionary.registerTransform({
  name: 'name/cti/kebab',
  type: 'name',
  transformer: (token) => {
    return token.path.join('-').toLowerCase();
  }
});
 
// Custom format for CSS custom properties
StyleDictionary.registerFormat({
  name: 'css/variables',
  formatter: (dictionary) => {
    return `:root {\n${dictionary.allProperties
      .map(prop => `  --${prop.name}: ${prop.value};`)
      .join('\n')}\n}`;
  }
});
 
// Custom format for TypeScript types
StyleDictionary.registerFormat({
  name: 'typescript/es6-declarations',
  formatter: (dictionary) => {
    const categories = {};
    
    dictionary.allProperties.forEach(prop => {
      const category = prop.attributes.category;
      if (!categories[category]) categories[category] = [];
      categories[category].push(prop);
    });
 
    let output = '// Auto-generated design tokens\n\n';
    
    Object.entries(categories).forEach(([category, tokens]) => {
      output += `export const ${category} = {\n`;
      tokens.forEach(token => {
        output += `  '${token.name}': '${token.value}',\n`;
      });
      output += '} as const;\n\n';
    });
 
    return output;
  }
});
 
const config = {
  source: ['tokens/**/*.json'],
  platforms: {
    css: {
      transformGroup: 'css',
      buildPath: 'dist/css/',
      files: [{
        destination: 'tokens.css',
        format: 'css/variables'
      }]
    },
    js: {
      transformGroup: 'js',
      buildPath: 'dist/js/',
      files: [{
        destination: 'tokens.js',
        format: 'javascript/es6'
      }, {
        destination: 'tokens.d.ts',
        format: 'typescript/es6-declarations'
      }]
    },
    json: {
      transformGroup: 'js',
      buildPath: 'dist/json/',
      files: [{
        destination: 'tokens.json',
        format: 'json/flat'
      }]
    }
  }
};
 
StyleDictionary.extend(config).buildAllPlatforms();

Runtime Token Management

// src/tokens/TokenProvider.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import { tokens } from '../generated/tokens';
 
interface TokenContextValue {
  tokens: typeof tokens;
  theme: 'light' | 'dark';
  setTheme: (theme: 'light' | 'dark') => void;
  getToken: (path: string) => string;
}
 
const TokenContext = createContext<TokenContextValue | undefined>(undefined);
 
interface TokenProviderProps {
  children: React.ReactNode;
  defaultTheme?: 'light' | 'dark';
}
 
export function TokenProvider({ 
  children, 
  defaultTheme = 'light' 
}: TokenProviderProps) {
  const [theme, setTheme] = useState<'light' | 'dark'>(defaultTheme);
 
  useEffect(() => {
    // Apply theme tokens to CSS variables
    const root = document.documentElement;
    const themeTokens = tokens.theme[theme];
    
    Object.entries(themeTokens).forEach(([key, value]) => {
      root.style.setProperty(`--${key}`, value);
    });
    
    // Apply theme class for CSS selectors
    root.className = root.className.replace(/theme-\w+/, '') + ` theme-${theme}`;
  }, [theme]);
 
  const getToken = (path: string): string => {
    const keys = path.split('.');
    let value: any = tokens;
    
    for (const key of keys) {
      value = value?.[key];
      if (value === undefined) {
        console.warn(`Token not found: ${path}`);
        return '';
      }
    }
    
    return value;
  };
 
  const value: TokenContextValue = {
    tokens,
    theme,
    setTheme,
    getToken,
  };
 
  return (
    <TokenContext.Provider value={value}>
      {children}
    </TokenContext.Provider>
  );
}
 
export function useTokens(): TokenContextValue {
  const context = useContext(TokenContext);
  if (!context) {
    throw new Error('useTokens must be used within a TokenProvider');
  }
  return context;
}
 
// Hook for easy token access
export function useToken(path: string): string {
  const { getToken } = useTokens();
  return getToken(path);
}

Component Architecture

Base Component System

// src/components/base/BaseComponent.tsx
import React from 'react';
import { useTokens } from '../../tokens/TokenProvider';
 
interface BaseComponentProps {
  children?: React.ReactNode;
  className?: string;
  testId?: string;
  as?: keyof JSX.IntrinsicElements;
}
 
export function BaseComponent({ 
  children, 
  className = '', 
  testId,
  as: Component = 'div',
  ...props 
}: BaseComponentProps) {
  const { getToken } = useTokens();
 
  return (
    <Component
      className={className}
      data-testid={testId}
      {...props}
    >
      {children}
    </Component>
  );
}
 
// Utility functions for component development
export function createStyledComponent<T extends Record<string, any>>(
  baseStyles: string,
  variants?: Record<string, Record<string, string>>,
  defaultVariant?: Partial<T>
) {
  return function StyledComponent({ 
    variant, 
    className = '', 
    ...props 
  }: T & { className?: string; variant?: string }) {
    let computedClassName = baseStyles;
 
    if (variants && variant) {
      const variantStyles = variants[variant];
      if (variantStyles) {
        computedClassName += ' ' + Object.values(variantStyles).join(' ');
      }
    }
 
    if (className) {
      computedClassName += ' ' + className;
    }
 
    return <BaseComponent className={computedClassName} {...props} />;
  };
}

Button Component Implementation

// src/components/Button/Button.tsx
import React, { forwardRef } from 'react';
import { BaseComponent } from '../base/BaseComponent';
import { useToken } from '../../tokens/TokenProvider';
import { Spinner } from '../Spinner/Spinner';
 
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
  size?: 'small' | 'medium' | 'large';
  loading?: boolean;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
  fullWidth?: boolean;
  children: React.ReactNode;
}
 
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
  variant = 'primary',
  size = 'medium',
  loading = false,
  leftIcon,
  rightIcon,
  fullWidth = false,
  disabled,
  className = '',
  children,
  ...props
}, ref) => {
  const primaryColor = useToken('color.semantic.primary');
  const dangerColor = useToken('color.semantic.danger');
  
  // Base styles using design tokens
  const baseStyles = `
    inline-flex items-center justify-center
    font-medium rounded-lg transition-all duration-200
    focus:outline-none focus:ring-2 focus:ring-offset-2
    disabled:opacity-50 disabled:cursor-not-allowed
    ${fullWidth ? 'w-full' : ''}
  `;
 
  // Variant styles
  const variantStyles = {
    primary: `
      bg-[var(--color-button-primary-background)]
      text-[var(--color-button-primary-text)]
      border border-[var(--color-button-primary-border)]
      hover:opacity-90 active:scale-95
      focus:ring-[var(--color-semantic-primary)]
    `,
    secondary: `
      bg-[var(--color-button-secondary-background)]
      text-[var(--color-button-secondary-text)]
      border border-[var(--color-button-secondary-border)]
      hover:bg-gray-50 active:scale-95
      focus:ring-[var(--color-semantic-primary)]
    `,
    outline: `
      bg-transparent text-[var(--color-semantic-primary)]
      border border-[var(--color-semantic-primary)]
      hover:bg-[var(--color-semantic-primary)] hover:text-white
      active:scale-95 focus:ring-[var(--color-semantic-primary)]
    `,
    ghost: `
      bg-transparent text-[var(--color-semantic-primary)]
      border border-transparent
      hover:bg-gray-100 active:scale-95
      focus:ring-[var(--color-semantic-primary)]
    `,
    danger: `
      bg-[var(--color-semantic-danger)] text-white
      border border-[var(--color-semantic-danger)]
      hover:opacity-90 active:scale-95
      focus:ring-[var(--color-semantic-danger)]
    `,
  };
 
  // Size styles
  const sizeStyles = {
    small: 'px-3 py-1.5 text-sm gap-1.5',
    medium: 'px-4 py-2 text-base gap-2',
    large: 'px-6 py-3 text-lg gap-2.5',
  };
 
  const computedClassName = `
    ${baseStyles}
    ${variantStyles[variant]}
    ${sizeStyles[size]}
    ${className}
  `.replace(/\s+/g, ' ').trim();
 
  return (
    <button
      ref={ref}
      className={computedClassName}
      disabled={disabled || loading}
      {...props}
    >
      {loading ? (
        <>
          <Spinner size={size === 'small' ? 'xs' : 'sm'} />
          Loading...
        </>
      ) : (
        <>
          {leftIcon && <span className="flex-shrink-0">{leftIcon}</span>}
          <span>{children}</span>
          {rightIcon && <span className="flex-shrink-0">{rightIcon}</span>}
        </>
      )}
    </button>
  );
});
 
Button.displayName = 'Button';

Input Component with Validation

// src/components/Input/Input.tsx
import React, { forwardRef, useState, useId } from 'react';
import { useToken } from '../../tokens/TokenProvider';
 
export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
  label?: string;
  error?: string;
  hint?: string;
  size?: 'small' | 'medium' | 'large';
  variant?: 'default' | 'filled' | 'outline';
  leftAddon?: React.ReactNode;
  rightAddon?: React.ReactNode;
  required?: boolean;
}
 
export const Input = forwardRef<HTMLInputElement, InputProps>(({
  label,
  error,
  hint,
  size = 'medium',
  variant = 'default',
  leftAddon,
  rightAddon,
  required = false,
  className = '',
  disabled,
  ...props
}, ref) => {
  const [focused, setFocused] = useState(false);
  const inputId = useId();
  const errorId = useId();
  const hintId = useId();
 
  // Base input styles
  const baseInputStyles = `
    w-full transition-all duration-200
    placeholder:text-gray-400
    disabled:opacity-50 disabled:cursor-not-allowed
    focus:outline-none
  `;
 
  // Variant styles
  const variantStyles = {
    default: `
      border border-gray-300 rounded-lg bg-white
      focus:border-[var(--color-semantic-primary)]
      focus:ring-2 focus:ring-[var(--color-semantic-primary)]/20
    `,
    filled: `
      border-0 bg-gray-100 rounded-lg
      focus:bg-white focus:ring-2 
      focus:ring-[var(--color-semantic-primary)]/20
    `,
    outline: `
      border-2 border-gray-200 rounded-lg bg-transparent
      focus:border-[var(--color-semantic-primary)]
    `,
  };
 
  // Size styles
  const sizeStyles = {
    small: 'px-3 py-2 text-sm',
    medium: 'px-4 py-2.5 text-base',
    large: 'px-5 py-3 text-lg',
  };
 
  // Error styles
  const errorStyles = error ? `
    border-red-300 focus:border-red-500 
    focus:ring-red-500/20
  ` : '';
 
  const inputClassName = `
    ${baseInputStyles}
    ${variantStyles[variant]}
    ${sizeStyles[size]}
    ${errorStyles}
    ${leftAddon ? 'pl-10' : ''}
    ${rightAddon ? 'pr-10' : ''}
    ${className}
  `.replace(/\s+/g, ' ').trim();
 
  return (
    <div className="space-y-1">
      {label && (
        <label 
          htmlFor={inputId}
          className="block text-sm font-medium text-gray-700"
        >
          {label}
          {required && <span className="text-red-500 ml-1">*</span>}
        </label>
      )}
      
      <div className="relative">
        {leftAddon && (
          <div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400">
            {leftAddon}
          </div>
        )}
        
        <input
          ref={ref}
          id={inputId}
          className={inputClassName}
          disabled={disabled}
          required={required}
          aria-invalid={!!error}
          aria-describedby={
            [error && errorId, hint && hintId]
              .filter(Boolean)
              .join(' ')
          }
          onFocus={(e) => {
            setFocused(true);
            props.onFocus?.(e);
          }}
          onBlur={(e) => {
            setFocused(false);
            props.onBlur?.(e);
          }}
          {...props}
        />
        
        {rightAddon && (
          <div className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400">
            {rightAddon}
          </div>
        )}
      </div>
      
      {error && (
        <p 
          id={errorId}
          className="text-sm text-red-600"
          role="alert"
        >
          {error}
        </p>
      )}
      
      {hint && !error && (
        <p 
          id={hintId}
          className="text-sm text-gray-500"
        >
          {hint}
        </p>
      )}
    </div>
  );
});
 
Input.displayName = 'Input';

Component Composition Patterns

Layout Components

// src/components/Layout/Stack.tsx
import React from 'react';
import { useToken } from '../../tokens/TokenProvider';
 
interface StackProps {
  children: React.ReactNode;
  direction?: 'row' | 'column';
  spacing?: 'small' | 'medium' | 'large' | 'none';
  align?: 'start' | 'center' | 'end' | 'stretch';
  justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
  wrap?: boolean;
  className?: string;
  as?: keyof JSX.IntrinsicElements;
}
 
export function Stack({
  children,
  direction = 'column',
  spacing = 'medium',
  align = 'stretch',
  justify = 'start',
  wrap = false,
  className = '',
  as: Component = 'div',
}: StackProps) {
  const gapSmall = useToken('spacing.semantic.gap.small');
  const gapMedium = useToken('spacing.semantic.gap.medium');
  const gapLarge = useToken('spacing.semantic.gap.large');
 
  const gapValues = {
    none: '0',
    small: gapSmall,
    medium: gapMedium,
    large: gapLarge,
  };
 
  const flexDirection = direction === 'row' ? 'flex-row' : 'flex-col';
  const flexWrap = wrap ? 'flex-wrap' : 'flex-nowrap';
  
  const alignItems = {
    start: 'items-start',
    center: 'items-center',
    end: 'items-end',
    stretch: 'items-stretch',
  };
 
  const justifyContent = {
    start: 'justify-start',
    center: 'justify-center',
    end: 'justify-end',
    between: 'justify-between',
    around: 'justify-around',
    evenly: 'justify-evenly',
  };
 
  const computedClassName = `
    flex ${flexDirection} ${flexWrap}
    ${alignItems[align]} ${justifyContent[justify]}
    ${className}
  `.replace(/\s+/g, ' ').trim();
 
  return (
    <Component 
      className={computedClassName}
      style={{ gap: gapValues[spacing] }}
    >
      {children}
    </Component>
  );
}
 
// src/components/Layout/Box.tsx
interface BoxProps {
  children?: React.ReactNode;
  padding?: 'small' | 'medium' | 'large' | 'none';
  margin?: 'small' | 'medium' | 'large' | 'none';
  background?: 'primary' | 'secondary' | 'surface' | 'transparent';
  radius?: 'small' | 'medium' | 'large' | 'full' | 'none';
  shadow?: 'small' | 'medium' | 'large' | 'none';
  className?: string;
  as?: keyof JSX.IntrinsicElements;
}
 
export function Box({
  children,
  padding = 'none',
  margin = 'none',
  background = 'transparent',
  radius = 'none',
  shadow = 'none',
  className = '',
  as: Component = 'div',
}: BoxProps) {
  const paddingClasses = {
    none: '',
    small: 'p-2',
    medium: 'p-4',
    large: 'p-6',
  };
 
  const marginClasses = {
    none: '',
    small: 'm-2',
    medium: 'm-4',
    large: 'm-6',
  };
 
  const backgroundClasses = {
    transparent: 'bg-transparent',
    primary: 'bg-[var(--color-semantic-primary)]',
    secondary: 'bg-gray-100',
    surface: 'bg-white',
  };
 
  const radiusClasses = {
    none: '',
    small: 'rounded-sm',
    medium: 'rounded-md',
    large: 'rounded-lg',
    full: 'rounded-full',
  };
 
  const shadowClasses = {
    none: '',
    small: 'shadow-sm',
    medium: 'shadow-md',
    large: 'shadow-lg',
  };
 
  const computedClassName = `
    ${paddingClasses[padding]}
    ${marginClasses[margin]}
    ${backgroundClasses[background]}
    ${radiusClasses[radius]}
    ${shadowClasses[shadow]}
    ${className}
  `.replace(/\s+/g, ' ').trim();
 
  return (
    <Component className={computedClassName}>
      {children}
    </Component>
  );
}

Compound Components

// src/components/Card/Card.tsx
import React, { createContext, useContext } from 'react';
import { Box } from '../Layout/Box';
import { Stack } from '../Layout/Stack';
 
interface CardContextValue {
  variant: 'elevated' | 'outlined' | 'filled';
  size: 'small' | 'medium' | 'large';
}
 
const CardContext = createContext<CardContextValue | undefined>(undefined);
 
interface CardProps {
  children: React.ReactNode;
  variant?: 'elevated' | 'outlined' | 'filled';
  size?: 'small' | 'medium' | 'large';
  className?: string;
  onClick?: () => void;
}
 
function CardRoot({
  children,
  variant = 'elevated',
  size = 'medium',
  className = '',
  onClick,
}: CardProps) {
  const variantStyles = {
    elevated: 'bg-white shadow-md hover:shadow-lg transition-shadow',
    outlined: 'bg-white border border-gray-200',
    filled: 'bg-gray-50',
  };
 
  const sizeStyles = {
    small: 'p-4',
    medium: 'p-6',
    large: 'p-8',
  };
 
  const baseStyles = 'rounded-lg transition-all duration-200';
  const interactiveStyles = onClick ? 'cursor-pointer hover:scale-[1.02]' : '';
 
  const computedClassName = `
    ${baseStyles}
    ${variantStyles[variant]}
    ${sizeStyles[size]}
    ${interactiveStyles}
    ${className}
  `.replace(/\s+/g, ' ').trim();
 
  const contextValue: CardContextValue = { variant, size };
 
  return (
    <CardContext.Provider value={contextValue}>
      <div className={computedClassName} onClick={onClick}>
        {children}
      </div>
    </CardContext.Provider>
  );
}
 
function CardHeader({ children, className = '' }: { children: React.ReactNode; className?: string }) {
  const context = useContext(CardContext);
  if (!context) {
    throw new Error('Card.Header must be used within a Card');
  }
 
  return (
    <div className={`border-b border-gray-200 pb-4 mb-4 ${className}`}>
      {children}
    </div>
  );
}
 
function CardTitle({ children, className = '' }: { children: React.ReactNode; className?: string }) {
  return (
    <h3 className={`text-lg font-semibold text-gray-900 ${className}`}>
      {children}
    </h3>
  );
}
 
function CardDescription({ children, className = '' }: { children: React.ReactNode; className?: string }) {
  return (
    <p className={`text-sm text-gray-600 mt-1 ${className}`}>
      {children}
    </p>
  );
}
 
function CardContent({ children, className = '' }: { children: React.ReactNode; className?: string }) {
  return (
    <div className={`${className}`}>
      {children}
    </div>
  );
}
 
function CardFooter({ children, className = '' }: { children: React.ReactNode; className?: string }) {
  const context = useContext(CardContext);
  if (!context) {
    throw new Error('Card.Footer must be used within a Card');
  }
 
  return (
    <div className={`border-t border-gray-200 pt-4 mt-4 ${className}`}>
      {children}
    </div>
  );
}
 
// Export compound component
export const Card = Object.assign(CardRoot, {
  Header: CardHeader,
  Title: CardTitle,
  Description: CardDescription,
  Content: CardContent,
  Footer: CardFooter,
});
 
// Usage example:
/*
<Card variant="elevated" size="medium" onClick={() => console.log('clicked')}>
  <Card.Header>
    <Card.Title>Product Name</Card.Title>
    <Card.Description>A brief description of the product</Card.Description>
  </Card.Header>
  <Card.Content>
    <p>Main content goes here...</p>
  </Card.Content>
  <Card.Footer>
    <Stack direction="row" justify="end" spacing="small">
      <Button variant="secondary">Cancel</Button>
      <Button variant="primary">Save</Button>
    </Stack>
  </Card.Footer>
</Card>
*/

Documentation and Developer Experience

Component Documentation

// src/components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
import { Heart, Download } from 'lucide-react';
 
const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  parameters: {
    layout: 'centered',
    docs: {
      description: {
        component: `
A versatile button component built with design tokens for consistent styling across the application.
Supports multiple variants, sizes, loading states, and icon compositions.
 
## Design Tokens Used
- \`color.button.primary.*\` for primary button styling
- \`color.button.secondary.*\` for secondary button styling
- \`spacing.semantic.padding.*\` for internal spacing
- \`typography.fontSize.*\` for text sizing
        `,
      },
    },
  },
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: { type: 'select' },
      options: ['primary', 'secondary', 'outline', 'ghost', 'danger'],
      description: 'Visual style variant of the button',
    },
    size: {
      control: { type: 'select' },
      options: ['small', 'medium', 'large'],
      description: 'Size of the button affecting padding and font size',
    },
    loading: {
      control: { type: 'boolean' },
      description: 'Shows loading spinner and disables interaction',
    },
    disabled: {
      control: { type: 'boolean' },
      description: 'Disables the button and reduces opacity',
    },
    fullWidth: {
      control: { type: 'boolean' },
      description: 'Makes the button take full width of its container',
    },
  },
};
 
export default meta;
type Story = StoryObj<typeof meta>;
 
// Basic variants
export const Primary: Story = {
  args: {
    children: 'Primary Button',
    variant: 'primary',
  },
};
 
export const Secondary: Story = {
  args: {
    children: 'Secondary Button',
    variant: 'secondary',
  },
};
 
export const Outline: Story = {
  args: {
    children: 'Outline Button',
    variant: 'outline',
  },
};
 
// Sizes
export const AllSizes: Story = {
  render: () => (
    <div className="space-y-4">
      <Button size="small">Small Button</Button>
      <Button size="medium">Medium Button</Button>
      <Button size="large">Large Button</Button>
    </div>
  ),
};
 
// With icons
export const WithIcons: Story = {
  render: () => (
    <div className="space-y-4">
      <Button leftIcon={<Heart size={16} />}>
        With Left Icon
      </Button>
      <Button rightIcon={<Download size={16} />}>
        With Right Icon
      </Button>
      <Button 
        leftIcon={<Heart size={16} />} 
        rightIcon={<Download size={16} />}
      >
        With Both Icons
      </Button>
    </div>
  ),
};
 
// Loading states
export const LoadingStates: Story = {
  render: () => (
    <div className="space-y-4">
      <Button loading>Loading Button</Button>
      <Button variant="secondary" loading>
        Loading Secondary
      </Button>
    </div>
  ),
};
 
// Interactive example
export const Interactive: Story = {
  args: {
    children: 'Click me!',
    onClick: () => alert('Button clicked!'),
  },
};

Component API Documentation

// docs/component-api.md generator
import fs from 'fs';
import path from 'path';
import { Project } from 'ts-morph';
 
function generateComponentDocs(componentPath: string) {
  const project = new Project();
  const sourceFile = project.addSourceFileAtPath(componentPath);
  
  const interfaces = sourceFile.getInterfaces();
  const components = sourceFile.getFunctions().filter(f => 
    f.getName()?.endsWith('Component') || 
    f.getReturnType().getText().includes('ReactElement')
  );
 
  let documentation = `# ${path.basename(componentPath, '.tsx')} API\n\n`;
 
  // Generate props documentation
  interfaces.forEach(interfaceDecl => {
    if (interfaceDecl.getName().includes('Props')) {
      documentation += `## Props\n\n`;
      documentation += `| Prop | Type | Default | Description |\n`;
      documentation += `|------|------|---------|-------------|\n`;
 
      interfaceDecl.getProperties().forEach(prop => {
        const name = prop.getName();
        const type = prop.getType().getText();
        const isOptional = prop.hasQuestionToken() ? 'Optional' : 'Required';
        const defaultValue = prop.getInitializer()?.getText() || '-';
        
        // Extract JSDoc description
        const jsDoc = prop.getJsDocs()[0];
        const description = jsDoc?.getDescription() || '';
 
        documentation += `| ${name} | \`${type}\` | ${defaultValue} | ${description} |\n`;
      });
 
      documentation += `\n`;
    }
  });
 
  // Generate usage examples
  documentation += `## Usage\n\n`;
  documentation += `\`\`\`tsx\n`;
  documentation += `import { ${path.basename(componentPath, '.tsx')} } from '@/components';\n\n`;
  documentation += `function Example() {\n`;
  documentation += `  return (\n`;
  documentation += `    <${path.basename(componentPath, '.tsx')}\n`;
  
  // Add common props as example
  interfaces.forEach(interfaceDecl => {
    if (interfaceDecl.getName().includes('Props')) {
      const commonProps = interfaceDecl.getProperties().slice(0, 3);
      commonProps.forEach(prop => {
        const name = prop.getName();
        documentation += `      ${name}="example"\n`;
      });
    }
  });
 
  documentation += `    >\n`;
  documentation += `      Content\n`;
  documentation += `    </${path.basename(componentPath, '.tsx')}>\n`;
  documentation += `  );\n`;
  documentation += `}\n`;
  documentation += `\`\`\`\n\n`;
 
  return documentation;
}
 
// Generate docs for all components
function generateAllComponentDocs() {
  const componentsDir = 'src/components';
  const outputDir = 'docs/components';
 
  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
  }
 
  function walkDirectory(dir: string) {
    const files = fs.readdirSync(dir);
    
    files.forEach(file => {
      const filePath = path.join(dir, file);
      const stat = fs.statSync(filePath);
      
      if (stat.isDirectory()) {
        walkDirectory(filePath);
      } else if (file.endsWith('.tsx') && !file.endsWith('.stories.tsx')) {
        const docs = generateComponentDocs(filePath);
        const outputPath = path.join(
          outputDir,
          `${path.basename(file, '.tsx')}.md`
        );
        fs.writeFileSync(outputPath, docs);
      }
    });
  }
 
  walkDirectory(componentsDir);
}

Testing Strategy

Component Testing

// src/components/Button/Button.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
import { TokenProvider } from '../../tokens/TokenProvider';
 
// Test wrapper with token provider
function TestWrapper({ children }: { children: React.ReactNode }) {
  return (
    <TokenProvider>
      {children}
    </TokenProvider>
  );
}
 
describe('Button', () => {
  it('renders with correct text', () => {
    render(
      <Button>Click me</Button>,
      { wrapper: TestWrapper }
    );
    
    expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
  });
 
  it('applies variant styles correctly', () => {
    render(
      <Button variant="primary" data-testid="primary-button">
        Primary
      </Button>,
      { wrapper: TestWrapper }
    );
    
    const button = screen.getByTestId('primary-button');
    expect(button).toHaveClass('bg-[var(--color-button-primary-background)]');
  });
 
  it('handles click events', async () => {
    const handleClick = jest.fn();
    const user = userEvent.setup();
    
    render(
      <Button onClick={handleClick}>Click me</Button>,
      { wrapper: TestWrapper }
    );
    
    await user.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
 
  it('shows loading state correctly', () => {
    render(
      <Button loading>Loading</Button>,
      { wrapper: TestWrapper }
    );
    
    const button = screen.getByRole('button');
    expect(button).toBeDisabled();
    expect(screen.getByText('Loading...')).toBeInTheDocument();
  });
 
  it('supports keyboard navigation', async () => {
    const handleClick = jest.fn();
    const user = userEvent.setup();
    
    render(
      <Button onClick={handleClick}>Click me</Button>,
      { wrapper: TestWrapper }
    );
    
    const button = screen.getByRole('button');
    button.focus();
    expect(button).toHaveFocus();
    
    await user.keyboard('{Enter}');
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
 
  it('renders icons correctly', () => {
    const LeftIcon = () => <span data-testid="left-icon"></span>;
    const RightIcon = () => <span data-testid="right-icon"></span>;
    
    render(
      <Button 
        leftIcon={<LeftIcon />} 
        rightIcon={<RightIcon />}
      >
        With Icons
      </Button>,
      { wrapper: TestWrapper }
    );
    
    expect(screen.getByTestId('left-icon')).toBeInTheDocument();
    expect(screen.getByTestId('right-icon')).toBeInTheDocument();
  });
 
  it('supports full width layout', () => {
    render(
      <Button fullWidth data-testid="full-width-button">
        Full Width
      </Button>,
      { wrapper: TestWrapper }
    );
    
    expect(screen.getByTestId('full-width-button')).toHaveClass('w-full');
  });
});

Integration Testing

// src/components/__tests__/integration.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TokenProvider } from '../tokens/TokenProvider';
import { Button } from '../Button/Button';
import { Input } from '../Input/Input';
import { Card } from '../Card/Card';
 
function TestApp() {
  const [inputValue, setInputValue] = React.useState('');
  const [submitted, setSubmitted] = React.useState(false);
 
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    setSubmitted(true);
  };
 
  return (
    <TokenProvider>
      <Card variant="elevated">
        <Card.Header>
          <Card.Title>Test Form</Card.Title>
          <Card.Description>Integration test form</Card.Description>
        </Card.Header>
        <Card.Content>
          <form onSubmit={handleSubmit}>
            <Input
              label="Name"
              value={inputValue}
              onChange={(e) => setInputValue(e.target.value)}
              required
            />
            <Button type="submit" disabled={!inputValue}>
              Submit
            </Button>
          </form>
          {submitted && (
            <p data-testid="success-message">
              Form submitted with: {inputValue}
            </p>
          )}
        </Card.Content>
      </Card>
    </TokenProvider>
  );
}
 
describe('Component Integration', () => {
  it('handles complete form workflow', async () => {
    const user = userEvent.setup();
    render(<TestApp />);
 
    // Check initial state
    expect(screen.getByText('Test Form')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Submit' })).toBeDisabled();
 
    // Fill form
    const input = screen.getByLabelText('Name *');
    await user.type(input, 'John Doe');
 
    // Submit should now be enabled
    expect(screen.getByRole('button', { name: 'Submit' })).toBeEnabled();
 
    // Submit form
    await user.click(screen.getByRole('button', { name: 'Submit' }));
 
    // Check success state
    expect(screen.getByTestId('success-message')).toHaveTextContent(
      'Form submitted with: John Doe'
    );
  });
 
  it('maintains design system consistency', () => {
    render(<TestApp />);
 
    // Check that components use design tokens
    const card = screen.getByText('Test Form').closest('[class*="bg-white"]');
    expect(card).toBeInTheDocument();
 
    const button = screen.getByRole('button');
    expect(button).toHaveClass('bg-[var(--color-button-primary-background)]');
  });
});

Build and Distribution

Library Build Configuration

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import postcss from 'rollup-plugin-postcss';
import { terser } from 'rollup-plugin-terser';
 
export default {
  input: 'src/index.ts',
  output: [
    {
      file: 'dist/index.cjs.js',
      format: 'cjs',
      sourcemap: true,
    },
    {
      file: 'dist/index.esm.js',
      format: 'esm',
      sourcemap: true,
    },
  ],
  plugins: [
    peerDepsExternal(),
    resolve(),
    commonjs(),
    typescript({
      tsconfig: './tsconfig.build.json',
      exclude: ['**/*.test.tsx', '**/*.stories.tsx'],
    }),
    postcss({
      extract: true,
      minimize: true,
      use: ['sass'],
    }),
    terser(),
  ],
  external: ['react', 'react-dom'],
};

Package Configuration

{
  "name": "@company/design-system",
  "version": "1.0.0",
  "description": "A comprehensive design system with tokens and components",
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist/",
    "tokens/"
  ],
  "scripts": {
    "build": "npm run build:tokens && npm run build:lib",
    "build:tokens": "node build/tokens.js",
    "build:lib": "rollup -c",
    "dev": "storybook dev -p 6006",
    "build:storybook": "storybook build",
    "test": "jest",
    "test:coverage": "jest --coverage",
    "lint": "eslint src --ext .ts,.tsx",
    "type-check": "tsc --noEmit"
  },
  "peerDependencies": {
    "react": ">=18.0.0",
    "react-dom": ">=18.0.0"
  },
  "devDependencies": {
    "@rollup/plugin-commonjs": "^24.0.0",
    "@rollup/plugin-node-resolve": "^15.0.0",
    "@rollup/plugin-typescript": "^11.0.0",
    "@storybook/react": "^7.0.0",
    "@testing-library/jest-dom": "^6.0.0",
    "@testing-library/react": "^14.0.0",
    "@testing-library/user-event": "^14.0.0",
    "jest": "^29.0.0",
    "rollup": "^3.0.0",
    "style-dictionary": "^3.8.0",
    "typescript": "^5.0.0"
  },
  "publishConfig": {
    "access": "public"
  }
}

Building a design system that teams actually want to use comes down to three key principles: start with solid tokens, build composable components, and prioritize developer experience. The investment in proper tooling, documentation, and testing pays dividends as your system scales across multiple projects and teams.

Remember: a design system isn't just about consistency—it's about enabling teams to build faster while maintaining high quality standards.