React Atomic Design: Component Architecture Patterns (2026)

As React apps grow, component organization becomes the difference between a codebase teams love and one they dread. Atomic Design — atoms, molecules, organisms, templates, pages — provides a vocabulary for thinking about component hierarchy. This guide covers Atomic Design in practice, its modern adaptations, feature-based architecture, and the folder structures used by production teams.

Atomic Design Levels

Brad Frost's Atomic Design maps chemistry's building blocks to UI components:

  • Atoms — smallest indivisible UI elements: Button, Input, Badge, Avatar, Icon, Spinner
  • Molecules — groups of atoms that work together: SearchBar (Input + Button), FormField (Label + Input + ErrorText), Card (Image + Text + Button)
  • Organisms — complex UI sections with their own logic: Header (Logo + Nav + SearchBar + Avatar), ProductGrid (filters + list of Cards), Sidebar
  • Templates — page layouts with placeholder content, no real data: DashboardLayout, AuthLayout, BlogLayout
  • Pages — templates filled with real data; this is what users see

Atoms

// src/components/atoms/Button/Button.tsx
import { forwardRef } from 'react'
import { clsx } from 'clsx'
import styles from './Button.module.css'

export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
  size?: 'xs' | 'sm' | 'md' | 'lg'
  loading?: boolean
  leftIcon?: React.ReactNode
  rightIcon?: React.ReactNode
  fullWidth?: boolean
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
  variant = 'primary',
  size = 'md',
  loading = false,
  leftIcon,
  rightIcon,
  fullWidth = false,
  disabled,
  className,
  children,
  ...props
}, ref) => (
  <button
    ref={ref}
    disabled={disabled || loading}
    className={clsx(
      styles.button,
      styles[variant],
      styles[size],
      fullWidth && styles.fullWidth,
      loading && styles.loading,
      className
    )}
    {...props}
  >
    {loading && <span className={styles.spinner} aria-hidden />}
    {!loading && leftIcon && <span className={styles.icon}>{leftIcon}</span>}
    <span>{children}</span>
    {!loading && rightIcon && <span className={styles.icon}>{rightIcon}</span>}
  </button>
))

Button.displayName = 'Button'

// Other atoms:
// src/components/atoms/Input/Input.tsx
// src/components/atoms/Badge/Badge.tsx
// src/components/atoms/Avatar/Avatar.tsx
// src/components/atoms/Spinner/Spinner.tsx
// src/components/atoms/Typography/Text.tsx
// src/components/atoms/Typography/Heading.tsx

Molecules

// src/components/molecules/SearchBar/SearchBar.tsx
import { useState, useCallback } from 'react'
import { Input } from '@/components/atoms/Input'
import { Button } from '@/components/atoms/Button'
import { SearchIcon } from '@/components/atoms/Icon'

interface SearchBarProps {
  onSearch: (query: string) => void
  placeholder?: string
  initialValue?: string
  isLoading?: boolean
}

export function SearchBar({ onSearch, placeholder = 'Search...', initialValue = '', isLoading }: SearchBarProps) {
  const [query, setQuery] = useState(initialValue)

  const handleSubmit = useCallback((e: React.FormEvent) => {
    e.preventDefault()
    if (query.trim()) onSearch(query.trim())
  }, [query, onSearch])

  return (
    <form onSubmit={handleSubmit} role="search" className="flex gap-2">
      <Input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder={placeholder}
        aria-label="Search query"
        leftElement={<SearchIcon />}
      />
      <Button type="submit" loading={isLoading}>Search</Button>
    </form>
  )
}

// src/components/molecules/FormField/FormField.tsx
interface FormFieldProps {
  label: string
  htmlFor: string
  error?: string
  hint?: string
  required?: boolean
  children: React.ReactNode
}

export function FormField({ label, htmlFor, error, hint, required, children }: FormFieldProps) {
  return (
    <div className="form-field">
      <label htmlFor={htmlFor}>
        {label}
        {required && <span aria-hidden> *</span>}
      </label>
      {children}
      {hint && !error && <p className="hint">{hint}</p>}
      {error && <p id={`${htmlFor}-error`} role="alert" className="error">{error}</p>}
    </div>
  )
}

// src/components/molecules/UserCard/UserCard.tsx
export function UserCard({ user, onEdit, onDelete }) {
  return (
    <article className="user-card">
      <Avatar src={user.avatarUrl} name={user.name} size="md" />
      <div>
        <Heading level={3}>{user.name}</Heading>
        <Text color="muted">{user.email}</Text>
        <Badge variant={user.status === 'active' ? 'success' : 'neutral'}>
          {user.status}
        </Badge>
      </div>
      <div className="actions">
        <Button variant="ghost" size="sm" onClick={() => onEdit(user)}>Edit</Button>
        <Button variant="danger" size="sm" onClick={() => onDelete(user.id)}>Delete</Button>
      </div>
    </article>
  )
}

Organisms

// src/components/organisms/Header/Header.tsx
// Organisms compose molecules and have their own state/data logic
import { SearchBar } from '@/components/molecules/SearchBar'
import { UserMenu } from '@/components/molecules/UserMenu'
import { Logo } from '@/components/atoms/Logo'
import { NavLink } from '@/components/atoms/NavLink'

export function Header() {
  const { user } = useAuth()
  const router = useRouter()

  return (
    <header className="header">
      <Logo />
      <nav aria-label="Main navigation">
        <NavLink href="/dashboard">Dashboard</NavLink>
        <NavLink href="/products">Products</NavLink>
        <NavLink href="/reports">Reports</NavLink>
      </nav>
      <SearchBar onSearch={q => router.push(`/search?q=${q}`)} />
      <UserMenu user={user} />
    </header>
  )
}

// src/components/organisms/UserTable/UserTable.tsx
export function UserTable() {
  const [filters, setFilters] = useState({ role: '', search: '' })
  const { data: users, isLoading } = useUsers(filters)

  return (
    <div>
      <UserTableFilters filters={filters} onChange={setFilters} />
      {isLoading ? <TableSkeleton /> : <UserTableBody users={users} />}
      <UserTablePagination />
    </div>
  )
}

Templates and Pages

// src/components/templates/DashboardLayout/DashboardLayout.tsx
// Template: layout structure with slots, no real data
interface DashboardLayoutProps {
  sidebar: React.ReactNode
  header: React.ReactNode
  children: React.ReactNode
}

export function DashboardLayout({ sidebar, header, children }: DashboardLayoutProps) {
  return (
    <div className="dashboard-layout">
      <aside className="sidebar">{sidebar}</aside>
      <div className="main">
        <header>{header}</header>
        <main>{children}</main>
      </div>
    </div>
  )
}

// src/app/dashboard/page.tsx — Next.js App Router page
// Page: real data + template = what users see
export default function DashboardPage() {
  return (
    <DashboardLayout
      sidebar={<AppSidebar />}
      header={<Header />}
    >
      <StatsGrid />
      <RevenueChart />
      <UserTable />
    </DashboardLayout>
  )
}

Folder Structure

src/
├── components/
│   ├── atoms/
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   ├── Button.module.css
│   │   │   ├── Button.test.tsx
│   │   │   ├── Button.stories.tsx
│   │   │   └── index.ts          ← re-export
│   │   ├── Input/
│   │   ├── Badge/
│   │   └── index.ts              ← re-export all atoms
│   ├── molecules/
│   │   ├── SearchBar/
│   │   ├── FormField/
│   │   └── index.ts
│   ├── organisms/
│   │   ├── Header/
│   │   ├── UserTable/
│   │   └── index.ts
│   └── templates/
│       ├── DashboardLayout/
│       └── AuthLayout/
├── features/                     ← Feature-based modules
│   ├── auth/
│   ├── users/
│   └── products/
├── hooks/                        ← Shared custom hooks
├── lib/                          ← Utilities, API clients
└── app/                          ← Next.js app directory

Feature-Based Architecture

For large apps, pure Atomic Design gets unwieldy. Feature-based architecture co-locates everything a feature needs:

src/features/users/
├── components/
│   ├── UserTable.tsx             ← Feature-specific components
│   ├── UserCard.tsx
│   └── UserFilters.tsx
├── hooks/
│   ├── useUsers.ts               ← Feature-specific hooks
│   └── useUserActions.ts
├── api/
│   └── users.ts                  ← API calls
├── stores/
│   └── userStore.ts              ← Zustand slice
├── types/
│   └── user.ts                   ← Feature types
└── index.ts                      ← Public API of the feature

// Only export what other features need
export { UserTable } from './components/UserTable'
export type { User } from './types/user'
// Internal components stay private — UserFilters not exported

Barrel Exports

// src/components/atoms/index.ts — barrel file
export { Button } from './Button'
export type { ButtonProps } from './Button'
export { Input } from './Input'
export type { InputProps } from './Input'
export { Badge } from './Badge'
export { Avatar } from './Avatar'
export { Spinner } from './Spinner'

// Clean imports throughout the app
import { Button, Input, Badge, Avatar } from '@/components/atoms'

// Instead of
import { Button } from '@/components/atoms/Button/Button'
import { Input } from '@/components/atoms/Input/Input'
Barrel export performance: Deep barrel files (re-exporting hundreds of items) can slow down build times because bundlers must parse the entire barrel to tree-shake it. In Next.js, enable optimizePackageImports in next.config.ts to mitigate this. For very large component libraries, prefer path imports over barrel imports.