DNA🏗️ System DesignComponent Architecture & Design Systems
🦖DinosaurSystem DesignComponentsDesign Systems

Component Architecture & Design Systems

From atoms to organisms, from tokens to themes — how to architect component libraries that scale across teams, brands, and years of evolution.

Component Architecture & Design Systems

A design system isn't a component library. A component library is code. A design system is an agreement between design and engineering about how interfaces are built. The architecture behind that agreement is what separates a pile of components from a scalable system.

Atomic Design: The Mental Model

Brad Frost's Atomic Design gives us a taxonomy for thinking about UI composition:

┌─────────────────────────────────────────────────────────────┐
│                        PAGES                                 │
│  Complete screens with real data                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │                    TEMPLATES                           │  │
│  │  Page-level layouts (slots for organisms)              │  │
│  │  ┌─────────────────────────────────────────────────┐  │  │
│  │  │                 ORGANISMS                        │  │  │
│  │  │  Complex UI sections (Header, Card, DataTable)   │  │  │
│  │  │  ┌───────────────────────────────────────────┐  │  │  │
│  │  │  │              MOLECULES                     │  │  │  │
│  │  │  │  Small groups (SearchBar, FormField)       │  │  │  │
│  │  │  │  ┌─────────────────────────────────────┐  │  │  │  │
│  │  │  │  │            ATOMS                     │  │  │  │  │
│  │  │  │  │  Button, Input, Label, Icon          │  │  │  │  │
│  │  │  │  └─────────────────────────────────────┘  │  │  │  │
│  │  │  └───────────────────────────────────────────┘  │  │  │
│  │  └─────────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

The Real Value

Atomic Design isn't about rigid classification — it's about dependency direction. Atoms depend on nothing. Molecules depend on atoms. Organisms depend on molecules. Violations of this direction create coupling that makes components impossible to reuse.

✅ Button (atom) → used by SearchBar (molecule) → used by Header (organism)
❌ Button imports Header styles → circular dependency, broken isolation

Component API Design

The API of a component — its props — is its contract with consumers. Bad APIs create maintenance nightmares at scale.

The Props Spectrum

Simple Props ──────────────────────────────────── Full Inversion of Control
  <Button color="red" />    │    <Listbox>{(state) => <CustomOption {...state} />}</Listbox>
  Easy to use               │    Maximum flexibility
  Limited flexibility       │    Higher complexity

Composition Over Configuration

Bad — the "god component" with 30 props:

<DataTable
  data={data}
  columns={columns}
  sortable
  filterable
  paginated
  pageSize={20}
  onSort={handleSort}
  onFilter={handleFilter}
  emptyState="No data"
  loadingState={<Spinner />}
  rowSelection="multi"
  onRowSelect={handleSelect}
  stickyHeader
  // ... 18 more props
/>

Good — composed from focused pieces:

<DataTable data={data}>
  <DataTable.Toolbar>
    <DataTable.Search />
    <DataTable.Filter column="status" />
  </DataTable.Toolbar>
  <DataTable.Header sticky>
    <DataTable.Column field="name" sortable />
    <DataTable.Column field="email" sortable />
    <DataTable.Column field="status" filterable />
  </DataTable.Header>
  <DataTable.Body>
    {(row) => (
      <DataTable.Row key={row.id} selectable>
        <DataTable.Cell>{row.name}</DataTable.Cell>
        <DataTable.Cell>{row.email}</DataTable.Cell>
        <DataTable.Cell><StatusBadge status={row.status} /></DataTable.Cell>
      </DataTable.Row>
    )}
  </DataTable.Body>
  <DataTable.Pagination pageSize={20} />
</DataTable>

Compound Components Pattern

Compound components share implicit state through React Context, allowing flexible composition without prop drilling:

const SelectContext = createContext<SelectState | null>(null);
 
function Select({ children, value, onChange }: SelectProps) {
  const [open, setOpen] = useState(false);
  return (
    <SelectContext.Provider value={{ value, onChange, open, setOpen }}>
      <div role="listbox">{children}</div>
    </SelectContext.Provider>
  );
}
 
function SelectTrigger({ children }: { children: React.ReactNode }) {
  const { value, open, setOpen } = useSelectContext();
  return (
    <button
      role="combobox"
      aria-expanded={open}
      onClick={() => setOpen(!open)}
    >
      {children ?? value}
    </button>
  );
}
 
function SelectOption({ value, children }: OptionProps) {
  const { value: selected, onChange, setOpen } = useSelectContext();
  return (
    <div
      role="option"
      aria-selected={value === selected}
      onClick={() => { onChange(value); setOpen(false); }}
    >
      {children}
    </div>
  );
}
 
Select.Trigger = SelectTrigger;
Select.Option = SelectOption;

Usage feels natural and flexible:

<Select value={country} onChange={setCountry}>
  <Select.Trigger>Choose country</Select.Trigger>
  <Select.Option value="us">United States</Select.Option>
  <Select.Option value="uk">United Kingdom</Select.Option>
</Select>

Headless UI Pattern

Separate behavior from presentation entirely. The component provides state and accessibility — consumers provide all rendering:

function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn(prev => !prev), []);
 
  return {
    on,
    toggle,
    getTogglerProps: (props?: any) => ({
      'aria-pressed': on,
      onClick: toggle,
      role: 'switch',
      ...props,
    }),
  };
}
 
function CustomSwitch() {
  const { on, getTogglerProps } = useToggle();
  return (
    <button {...getTogglerProps()} className={on ? 'active' : 'inactive'}>
      {on ? '🌙' : '☀️'}
    </button>
  );
}

Libraries like Radix UI, Headless UI, and React Aria popularized this pattern. It's the right choice when visual customization is a primary requirement.

Design Token Architecture

Tokens are the atoms of your design system — named values that encode design decisions.

Token Hierarchy

┌────────────────────────────────────────────────────────┐
│  Global Tokens (primitives)                            │
│  blue-500: #3B82F6    spacing-4: 16px    radius-md: 8 │
│                                                        │
│  ┌──────────────────────────────────────────────────┐  │
│  │  Semantic Tokens (aliases with meaning)           │  │
│  │  color-primary: {blue-500}                        │  │
│  │  color-danger: {red-500}                          │  │
│  │  spacing-component-gap: {spacing-4}               │  │
│  │                                                   │  │
│  │  ┌────────────────────────────────────────────┐   │  │
│  │  │  Component Tokens (scoped)                 │   │  │
│  │  │  button-bg: {color-primary}                │   │  │
│  │  │  button-radius: {radius-md}                │   │  │
│  │  │  input-border: {color-border}              │   │  │
│  │  └────────────────────────────────────────────┘   │  │
│  └──────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────┘

Implementation with CSS Custom Properties

:root {
  /* Global tokens */
  --color-blue-500: #3b82f6;
  --color-red-500: #ef4444;
  --spacing-4: 1rem;
  --radius-md: 0.5rem;
  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
 
  /* Semantic tokens */
  --color-primary: var(--color-blue-500);
  --color-danger: var(--color-red-500);
  --color-bg: #ffffff;
  --color-text: #111827;
  --color-border: #e5e7eb;
}
 
[data-theme="dark"] {
  --color-bg: #0f172a;
  --color-text: #f1f5f9;
  --color-border: #334155;
  --color-primary: #60a5fa;
}

Why three layers? Changing blue-500 updates the primitive. Changing color-primary rebrands the entire app. Changing button-bg affects only buttons. Each layer gives you a different blast radius.

Theming Architecture

CSS Variables + Theme Context

type Theme = 'light' | 'dark' | 'system';
 
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>(() => {
    return (localStorage.getItem('theme') as Theme) ?? 'system';
  });
 
  useEffect(() => {
    const resolved = theme === 'system'
      ? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
      : theme;
    document.documentElement.setAttribute('data-theme', resolved);
    localStorage.setItem('theme', theme);
  }, [theme]);
 
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

Multi-Brand / White-Label Architecture

┌──────────────────────────────────────────┐
│         Brand Configuration              │
│                                          │
│  brand-a/                                │
│    tokens.css    ← colors, fonts, radii  │
│    logo.svg                              │
│    overrides.css ← component overrides   │
│                                          │
│  brand-b/                                │
│    tokens.css                            │
│    logo.svg                              │
│    overrides.css                         │
│                                          │
│  core/                                   │
│    components/   ← brand-agnostic        │
│    tokens.css    ← default/fallback      │
└──────────────────────────────────────────┘

Load brand tokens at build time or runtime:

async function loadBrandTheme(brandId: string) {
  const tokens = await import(`./brands/${brandId}/tokens.css`);
  const overrides = await import(`./brands/${brandId}/overrides.css`);
  document.documentElement.setAttribute('data-brand', brandId);
}

Component Library Versioning

Semantic Versioning for Components

ChangeVersion BumpExample
New prop (optional)MinorAdding size="xl" to Button
Bug fixPatchFixing focus ring on Input
Remove propMajorRemoving variant="ghost"
Rename componentMajorDropdown → Select
Change default valueMajorButton size default md → sm

Managing Breaking Changes

v2.0.0-beta.1   → Early adopters test
v2.0.0-rc.1     → Feature complete, stabilizing
v2.0.0          → Release
 
Codemods:        Automated migration scripts
Deprecation:     Warn for 2 minor versions before removing
Adapter:         <ButtonV1 /> wrapper that maps old API to new

Storybook as Living Documentation

src/
  components/
    Button/
      Button.tsx
      Button.stories.tsx    ← Visual documentation
      Button.test.tsx       ← Unit tests
      Button.module.css     ← Styles

Story Structure

import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
 
const meta: Meta<typeof Button> = {
  component: Button,
  argTypes: {
    variant: { control: 'select', options: ['primary', 'secondary', 'ghost'] },
    size: { control: 'select', options: ['sm', 'md', 'lg'] },
    disabled: { control: 'boolean' },
  },
};
export default meta;
 
type Story = StoryObj<typeof Button>;
 
export const Primary: Story = {
  args: { variant: 'primary', children: 'Click me' },
};
 
export const AllVariants: Story = {
  render: () => (
    <div style={{ display: 'flex', gap: '1rem' }}>
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="ghost">Ghost</Button>
    </div>
  ),
};

Accessibility in Design Systems

Accessibility built into the design system means every consumer gets it for free.

The Accessibility Contract

Every interactive component must provide:

  1. Keyboard support — Tab, Enter, Space, Escape, Arrow keys where appropriate
  2. ARIA attributes — role, aria-label, aria-expanded, aria-selected
  3. Focus management — Visible focus ring, logical focus order, focus trapping in modals
  4. Color independence — Information conveyed by more than just color
function Checkbox({ checked, onChange, label, id }: CheckboxProps) {
  return (
    <label htmlFor={id} className="checkbox-label">
      <input
        type="checkbox"
        id={id}
        checked={checked}
        onChange={(e) => onChange(e.target.checked)}
        aria-checked={checked}
      />
      <span className="checkbox-indicator" aria-hidden="true">
        {checked && <CheckIcon />}
      </span>
      <span>{label}</span>
    </label>
  );
}

Decision Framework: When to Build vs Adopt

FactorBuild In-HouseAdopt (Radix, MUI, etc.)
Brand uniquenessHigh — need custom lookLow — standard UI is fine
Team sizeLarge — can maintainSmall — can't maintain
TimelineLong — can investShort — need it now
Accessibility expertiseHave itDon't have it — let library handle
Bundle size sensitivityHigh — want tree-shaking controlModerate — accept library weight

The senior answer is almost always: start with a headless library (Radix, React Aria) and build your visual layer on top. You get battle-tested accessibility and keyboard handling without inheriting someone else's visual opinions.

Architecture Diagram: Full Design System

┌─────────────────────────────────────────────────────┐
│                    Consumers                         │
│  App A        App B        App C        Storybook   │
│    │            │            │              │        │
│    └────────────┴────────────┴──────────────┘        │
│                      │                               │
│         ┌────────────▼───────────────┐               │
│         │    @org/design-system       │               │
│         │    (npm package)            │               │
│         │                            │               │
│         │  ┌────────────────────┐    │               │
│         │  │  Components        │    │               │
│         │  │  (atoms → organisms)│   │               │
│         │  └─────────┬──────────┘    │               │
│         │            │               │               │
│         │  ┌─────────▼──────────┐    │               │
│         │  │  Design Tokens      │    │               │
│         │  │  (CSS vars + TS)    │    │               │
│         │  └─────────┬──────────┘    │               │
│         │            │               │               │
│         │  ┌─────────▼──────────┐    │               │
│         │  │  Headless Layer     │    │               │
│         │  │  (Radix / React Aria)│   │               │
│         │  └────────────────────┘    │               │
│         └────────────────────────────┘               │
└─────────────────────────────────────────────────────┘

Design systems are infrastructure. Architect them like infrastructure — with versioning, contracts, documentation, and a clear migration path. The best component library is one where teams don't fight it; they reach for it naturally.