Design System Component

Category: Design Systems October 15, 2025

Create reusable, accessible design system components with documentation and variants.

Design SystemsComponentsUI/UXAccessibilityReusability
# Design System Component

Create a production-ready, reusable component for a design system with variants, accessibility, documentation, and tests.

## Component Requirements

### 1. API Design
- Props interface with TypeScript types
- Sensible defaults
- Flexible but not over-engineered
- Composable with other components
- Support for ref forwarding

### 2. Variants
- Size variations (small, medium, large)
- Style variations (primary, secondary, outline, ghost)
- State variations (default, hover, active, disabled, loading)
- Theme support (light, dark)

### 3. Accessibility
- Proper ARIA attributes
- Keyboard navigation support
- Focus management
- Screen reader friendly
- High contrast mode support
- WCAG 2.1 Level AA compliance

### 4. Styling
- CSS-in-JS or Tailwind classes
- Responsive design
- Animation and transitions
- Theme tokens integration
- CSS variables for customization

## Component Structure

```tsx
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
}

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ 
    children, 
    variant = 'primary', 
    size = 'md',
    isLoading = false,
    disabled,
    leftIcon,
    rightIcon,
    className,
    ...props 
  }, ref) => {
    const baseStyles = "inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
    
    const variants = {
      primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
      secondary: "bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500",
      outline: "border border-gray-300 bg-transparent hover:bg-gray-50",
      ghost: "bg-transparent hover:bg-gray-100"
    };
    
    const sizes = {
      sm: "px-3 py-1.5 text-sm",
      md: "px-4 py-2 text-base",
      lg: "px-6 py-3 text-lg"
    };
    
    return (
      <button
        ref={ref}
        className={cn(
          baseStyles,
          variants[variant],
          sizes[size],
          (disabled || isLoading) && "opacity-50 cursor-not-allowed",
          className
        )}
        disabled={disabled || isLoading}
        aria-busy={isLoading}
        {...props}
      >
        {isLoading && <Spinner className="mr-2" />}
        {leftIcon && <span className="mr-2">{leftIcon}</span>}
        {children}
        {rightIcon && <span className="ml-2">{rightIcon}</span>}
      </button>
    );
  }
);

Button.displayName = 'Button';

Documentation

Storybook Stories

export default {
  title: 'Components/Button',
  component: Button,
} as Meta;

export const Primary = () => <Button variant="primary">Click me</Button>;
export const Secondary = () => <Button variant="secondary">Click me</Button>;
export const WithIcons = () => (
  <Button leftIcon={<IconPlus />} rightIcon={<IconArrow />}>
    Add Item
  </Button>
);
export const Loading = () => <Button isLoading>Loading...</Button>;

Usage Examples

// Basic usage
<Button onClick={handleClick}>Submit</Button>

// With variant and size
<Button variant="primary" size="lg">Large Primary Button</Button>

// With icon
<Button leftIcon={<IconSave />}>Save Changes</Button>

// Loading state
<Button isLoading disabled>Saving...</Button>

Testing

describe('Button', () => {
  it('renders children correctly', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });
  
  it('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);
    fireEvent.click(screen.getByText('Click me'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
  
  it('is disabled when isLoading is true', () => {
    render(<Button isLoading>Click me</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
  });
  
  it('is accessible via keyboard', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);
    const button = screen.getByRole('button');
    button.focus();
    fireEvent.keyDown(button, { key: 'Enter' });
    expect(handleClick).toHaveBeenCalled();
  });
});

Component Checklist

  • TypeScript types defined
  • All variants implemented
  • Accessible (ARIA, keyboard)
  • Responsive design
  • Theme support
  • Documentation written
  • Storybook stories created
  • Tests written (>80% coverage)
  • Peer reviewed
  • Design approved