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

LevelDefinitionReact ExampleAngular ExampleSide Effects?
AtomSmallest UI unit with no dependencies on other components<Button />, <Input />, <Badge />ButtonComponent, InputComponentNo
MoleculeCombination of atoms that function together as a unit<SearchBar> = <Input> + <Button>SearchBarComponentNo
OrganismComplex UI section combining molecules and atoms with business context<Header> = nav + logo + search barHeaderComponent with projected contentYes (may inject services)
TemplatePage-level layout structure with content slots — no real data<BlogLayout> with {children} slotsBlogLayoutComponent with <ng-content>No (structural only)
PageSpecific instance of a template populated with real content/data<BlogPost post={data} />BlogPostComponent with resolved route dataYes (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 tests

Key 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 ModuleBehaviourTypical Use
OverlayModulePositioning overlaysTooltips, dropdowns, popovers
DragDropModuleDrag-and-dropSortable lists, kanban boards
A11yModuleFocus trap, live announcerModal dialogs, accessible alerts
PortalModuleRender outside current DOM hierarchyDynamic overlays, portals
ScrollingModuleVirtual scrollingLarge 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.