Back to Design Systems
Topic 3 of 8

Component Libraries

Build reusable UI components with React, Vue, and framework-agnostic approaches

What is a Component Library?

A component library is a collection of reusable UI components that implement your design system. Components are the building blocks of your interfaceβ€”buttons, inputs, modals, cards, and moreβ€”packaged for consistent use across your applications.

πŸ“¦ Component Library Approaches

Styled Components

Pre-styled, opinionated (Ant Design, MUI)

Headless/Unstyled

Logic only, you add styles (Radix, Headless UI)

Copy-Paste

Components you own (shadcn/ui)

Building a Basic Component

// components/Button/Button.tsx
import React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const buttonVariants = cva(
  // Base styles
  'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        primary: 'bg-primary text-white hover:bg-primary/90',
        secondary: 'bg-secondary text-white hover:bg-secondary/90',
        outline: 'border border-input bg-transparent hover:bg-accent',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
        destructive: 'bg-destructive text-white hover:bg-destructive/90',
      },
      size: {
        sm: 'h-8 px-3 text-xs',
        md: 'h-10 px-4 text-sm',
        lg: 'h-12 px-6 text-base',
        icon: 'h-10 w-10',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes,
    VariantProps {
  isLoading?: boolean;
}

export const Button = React.forwardRef(
  ({ className, variant, size, isLoading, children, disabled, ...props }, ref) => {
    return (
      
    );
  }
);

Button.displayName = 'Button';

// Usage

Compound Components Pattern

For complex components like dropdowns or tabs, use the compound component pattern for flexibility:

// components/Tabs/Tabs.tsx
import React, { createContext, useContext, useState } from 'react';

// Context for shared state
const TabsContext = createContext<{
  activeTab: string;
  setActiveTab: (id: string) => void;
} | null>(null);

// Root component
export function Tabs({ defaultValue, children }: { 
  defaultValue: string; 
  children: React.ReactNode;
}) {
  const [activeTab, setActiveTab] = useState(defaultValue);
  
  return (
    
      
{children}
); } // Tab list export function TabList({ children }: { children: React.ReactNode }) { return (
{children}
); } // Individual tab trigger export function TabTrigger({ value, children }: { value: string; children: React.ReactNode; }) { const context = useContext(TabsContext); if (!context) throw new Error('TabTrigger must be used within Tabs'); const { activeTab, setActiveTab } = context; const isActive = activeTab === value; return ( ); } // Tab content panel export function TabContent({ value, children }: { value: string; children: React.ReactNode; }) { const context = useContext(TabsContext); if (!context) throw new Error('TabContent must be used within Tabs'); if (context.activeTab !== value) return null; return (
{children}
); } // Usage - Clean, flexible API Account Settings Notifications Account content here... Settings content here... Notifications content here...

Headless Components

Headless components provide behavior and accessibility without styling. Radix UI is the most popular choice:

// Using Radix UI Primitives
import * as Dialog from '@radix-ui/react-dialog';
import { X } from 'lucide-react';

export function Modal({ trigger, title, children }) {
  return (
    
      
        {trigger}
      
      
      
        
        
        
          
            {title}
          
          
          {children}
          
          
            
          
        
      
    
  );
}

// Usage
Open Modal}
  title="Edit Profile"
>
  

Modal content here...

Component Organization

components/
β”œβ”€β”€ primitives/          # Base building blocks
β”‚   β”œβ”€β”€ Button/
β”‚   β”‚   β”œβ”€β”€ Button.tsx
β”‚   β”‚   β”œβ”€β”€ Button.test.tsx
β”‚   β”‚   β”œβ”€β”€ Button.stories.tsx
β”‚   β”‚   └── index.ts
β”‚   β”œβ”€β”€ Input/
β”‚   └── Select/
β”œβ”€β”€ composed/            # Built from primitives
β”‚   β”œβ”€β”€ SearchBar/       # Input + Button
β”‚   β”œβ”€β”€ Combobox/        # Input + Select + List
β”‚   └── DatePicker/
β”œβ”€β”€ patterns/            # Common UI patterns
β”‚   β”œβ”€β”€ DataTable/
β”‚   β”œβ”€β”€ Form/
β”‚   └── Sidebar/
└── index.ts             # Barrel exports

// index.ts - Export everything
export * from './primitives/Button';
export * from './primitives/Input';
export * from './composed/SearchBar';
// ...

πŸ’‘ Best Practices

  • β€’ Use forwardRef so components work with form libraries and refs
  • β€’ Make components accessible by default (ARIA, keyboard navigation)
  • β€’ Use composition over configurationβ€”prefer compound components
  • β€’ Consider headless libraries (Radix, React Aria) for complex interactions
  • β€’ Write stories in Storybook for every component variation
  • β€’ Test components in isolation with unit tests and visual regression tests