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.
Table of Contents
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.