Atomic Design Pattern
Atomic Design Pattern
Brad Frost's five-level component hierarchy — atoms, molecules, organisms, templates, pages — applied as a composition model for design systems, not a folder structure prescription.
Intent
Brad Frost's atomic design methodology (2013) provides a shared mental model for breaking UI into a hierarchy of reusable components. It answers: how do components compose upward from primitives to full pages?
The hierarchy maps naturally to the Composite-Pattern — atoms and molecules form a Composite tree, organisms aggregate them, templates define layout structure, pages instantiate templates with real data. The atomic design levels are composition relationships, not file system categories.
Cross-ecosystem: the hierarchy applies equally to React component trees and Angular component trees. Atoms are ButtonComponent or <Button />; organisms are HeaderComponent or <Header />; the vocabulary is framework-agnostic.
Wikilinks: Component-Composition-Patterns, Micro-Frontends
When NOT to Use
- Prototypes / MVPs where component reuse is not yet a concern — atomic design adds indirection before the component boundaries are understood. Establish the product first.
- Single-developer projects with < 20 components — the hierarchy adds naming overhead without proportional reuse benefit. A flat
components/folder is clearer at this scale. - As a folder structure prescription: do NOT create
src/atoms/,src/molecules/directories — this rigidly maps a design concept to the build system and creates friction as components evolve between levels. A molecule promoted to an organism requires moving files and updating all imports. - When the team has no design system — atomic design describes design system component relationships. Without a design system, the hierarchy has nothing to organise. The vocabulary is meaningless without shared component ownership.
When to Use
- Building or maintaining a design system shared across multiple applications or teams — atomic design provides the shared vocabulary for component ownership and classification.
- Component library with 50+ components that needs a shared vocabulary for classification — at this scale, informal naming breaks down and the hierarchy provides a principled structure.
- Multi-team frontend where teams must agree on component ownership boundaries — organisms belong to feature teams; atoms/molecules belong to the shared design system team.
- Storybook or Angular CDK-based component catalog needs a structural organisation model — atomic design hierarchy maps directly to Storybook sidebar organisation (Atoms/, Molecules/, Organisms/).
The Hierarchy
| Level | Definition | React Example | Angular Example | Side Effects? |
|---|---|---|---|---|
| Atom | Smallest UI unit with no dependencies on other components | <Button />, <Input />, <Badge /> | ButtonComponent, InputComponent | No |
| Molecule | Combination of atoms that function together as a unit | <SearchBar> = <Input> + <Button> | SearchBarComponent | No |
| Organism | Complex UI section combining molecules and atoms with business context | <Header> = nav + logo + search bar | HeaderComponent with projected content | Yes (may inject services) |
| Template | Page-level layout structure with content slots — no real data | <BlogLayout> with {children} slots | BlogLayoutComponent with <ng-content> | No (structural only) |
| Page | Specific instance of a template populated with real content/data | <BlogPost post={data} /> | BlogPostComponent with resolved route data | Yes (data fetching) |
Boundary rule: Atoms and molecules have no side effects and no direct service/store dependencies. Side effects belong in organisms and above. This is the critical boundary that prevents scope creep.
Composition Diagram
See Atomic Design Composition Diagram for a visual hierarchy showing composition layers, example components at each level, and design system integration points.
Design System Integration
Storybook (React + Angular)
Storybook 8.x provides a component catalog that maps naturally to atomic design levels. Each story file documents a component at its hierarchy level. Stories are organised by level in the Storybook sidebar (Atoms/, Molecules/, Organisms/) — this is where atomic design as folder structure is appropriate (story organisation, not source code).
CSF3 format (Component Story Format 3):
// React — Atoms/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Atoms/Button',
component: Button,
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: { variant: 'primary', children: 'Click me' },
};// Angular — Atoms/Button.stories.ts
import type { Meta, StoryObj } from '@storybook/angular';
import { applicationConfig } from '@storybook/angular';
import { provideAnimations } from '@angular/platform-browser/animations';
import { ButtonComponent } from './button.component';
const meta: Meta<ButtonComponent> = {
title: 'Atoms/Button',
component: ButtonComponent,
decorators: [applicationConfig({ providers: [provideAnimations()] })],
};
export default meta;
type Story = StoryObj<ButtonComponent>;
export const Primary: Story = {
args: { variant: 'primary' },
};composeStories enables reuse of stories in tests (React):
import { composeStories } from '@storybook/react';
import * as stories from './Button.stories';
const { Primary } = composeStories(stories);
// Primary is a fully configured component ready to render in testsKey insight: Storybook organises stories by hierarchy level (Atoms/, Molecules/, Organisms/) in the sidebar — this is where atomic design AS folder structure is appropriate (story organisation, not source code).
Angular CDK
Angular CDK (@angular/cdk) provides behaviour primitives without styling — the foundation for building atoms. CDK sits at the atom/molecule boundary: it provides the unstyled behaviour that atoms wrap with design system styling.
| CDK Module | Behaviour | Typical Use |
|---|---|---|
OverlayModule | Positioning overlays | Tooltips, dropdowns, popovers |
DragDropModule | Drag-and-drop | Sortable lists, kanban boards |
A11yModule | Focus trap, live announcer | Modal dialogs, accessible alerts |
PortalModule | Render outside current DOM hierarchy | Dynamic overlays, portals |
ScrollingModule | Virtual scrolling | Large lists (1000+ items) |
// An atom using CDK A11yModule — accessible button with focus indicator
import { Component, inject } from '@angular/core';
import { FocusMonitor } from '@angular/cdk/a11y';
@Component({
selector: 'app-button',
standalone: true,
template: `<button #btn>{{ label }}</button>`,
})
export class ButtonComponent {
private focusMonitor = inject(FocusMonitor);
// CDK FocusMonitor provides keyboard vs. mouse focus distinction
// Used to style focus ring only for keyboard navigation
}Key insight: CDK is not a component library; it is the behaviour layer that atoms consume. A design system wraps CDK primitives with brand-specific styling — the CDK handles accessibility, keyboard navigation, and overlay positioning while the atom owns visual presentation.
TS Examples
Atom Example (React + Angular)
// React — Button atom
interface ButtonProps {
variant: 'primary' | 'secondary' | 'ghost';
size: 'sm' | 'md' | 'lg';
onClick?: () => void;
children: React.ReactNode;
}
export const Button: React.FC<ButtonProps> = ({
variant,
size,
onClick,
children,
}) => (
<button
className={`btn btn-${variant} btn-${size}`}
onClick={onClick}
>
{children}
</button>
);
// No side effects, no service dependencies — pure UI// Angular — ButtonComponent atom
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-button',
standalone: true,
imports: [CommonModule],
template: `
<button
[class]="'btn btn-' + variant + ' btn-' + size"
(click)="clicked.emit()"
>
<ng-content></ng-content>
</button>
`,
})
export class ButtonComponent {
@Input() variant: 'primary' | 'secondary' | 'ghost' = 'primary';
@Input() size: 'sm' | 'md' | 'lg' = 'md';
@Output() clicked = new EventEmitter<void>();
// No inject() calls — no service dependencies
}Molecule Example (React + Angular)
// React — SearchBar molecule (composes Input + Button atoms)
import { useState } from 'react';
import { Input } from './Input';
import { Button } from './Button';
interface SearchBarProps {
onSearch: (query: string) => void;
}
export const SearchBar: React.FC<SearchBarProps> = ({ onSearch }) => {
const [query, setQuery] = useState('');
return (
<div className="search-bar">
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<Button variant="primary" size="md" onClick={() => onSearch(query)}>
Search
</Button>
</div>
);
};
// Local state only — no external service calls// Angular — SearchBarComponent molecule
import { Component, Output, EventEmitter } from '@angular/core';
import { ButtonComponent } from './button.component';
import { InputComponent } from './input.component';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-search-bar',
standalone: true,
imports: [ButtonComponent, InputComponent, FormsModule],
template: `
<div class="search-bar">
<app-input [(ngModel)]="query" placeholder="Search..."></app-input>
<app-button variant="primary" (clicked)="onSearch()">Search</app-button>
</div>
`,
})
export class SearchBarComponent {
query = '';
@Output() search = new EventEmitter<string>();
onSearch(): void {
this.search.emit(this.query);
}
// No inject() for external services — local state only
}Organism Example (React + Angular)
// React — Header organism (composes Logo atom + SearchBar molecule + NavMenu molecule)
import { useFetch } from '../hooks/useFetch';
import { Logo } from './Logo';
import { SearchBar } from './SearchBar';
import { NavMenu } from './NavMenu';
interface User { id: string; name: string; avatarUrl: string; }
export const Header: React.FC = () => {
// Side effects belong at organism level and above
const { data: user } = useFetch<User>('/api/me');
return (
<header className="header">
<Logo />
<SearchBar onSearch={(q) => console.log('search:', q)} />
<NavMenu user={user} />
</header>
);
};// Angular — HeaderComponent organism
import { Component, inject } from '@angular/core';
import { LogoComponent } from './logo.component';
import { SearchBarComponent } from './search-bar.component';
import { NavMenuComponent } from './nav-menu.component';
import { AuthService } from '../services/auth.service';
@Component({
selector: 'app-header',
standalone: true,
imports: [LogoComponent, SearchBarComponent, NavMenuComponent],
template: `
<header class="header">
<app-logo></app-logo>
<app-search-bar (search)="onSearch($event)"></app-search-bar>
<app-nav-menu [user]="user$ | async"></app-nav-menu>
<ng-content></ng-content>
</header>
`,
})
export class HeaderComponent {
// inject() at organism level — service dependency is appropriate here
private authService = inject(AuthService);
user$ = this.authService.currentUser$;
onSearch(query: string): void {
console.log('search:', query);
}
}Anti-Patterns
Atomic design as file system:
Do not create src/atoms/, src/molecules/ directories in source code. The classification describes design intent, not physical organisation. Components evolve between levels; a molecule that grows to include a service call becomes an organism — renaming its directory cascades through all import paths. Use atomic vocabulary in documentation and Storybook story organisation, not in file paths.
Scope creep — atoms with API calls:
An atom that calls inject(HttpClient) or uses useQuery() has crossed the side-effects boundary. The atom is now coupled to your API layer and cannot be rendered in isolation in Storybook without mocking external dependencies. If a component needs data, promote it to organism or provide the data via props from an organism above it.
// Anti-pattern — atom with API call
@Component({ selector: 'app-badge' })
export class BadgeComponent {
private http = inject(HttpClient); // WRONG: atom with service dependency
count$ = this.http.get<number>('/api/notifications/count');
}
// Correct — receive count as Input from an organism above
@Component({ selector: 'app-badge' })
export class BadgeComponent {
@Input() count = 0; // Data flows down from organism
}Level rigidity:
The classification is descriptive, not prescriptive. A SearchBar that today composes two atoms may tomorrow need to call an autocomplete API — it becomes an organism. Do not resist reclassification. The hierarchy describes current composition behaviour, not permanent identity.