{product.name}
{/* Atom */}{product.description}
{/* Atom */} <PriceDisplay price={product.price} /> {/* Molecule */} <Button onClick={() => addToCart(product)}>Add to Cart</Button> {/* Atom */}
- Design methodology inspired by chemistry: atoms → molecules → organisms → templates → pages
- Creates scalable, reusable component design systems
- Enables concurrent creation of UI and design system
Atomic Design isn’t a step-by-step process (“first make all atoms, then molecules…”)—it’s a mental model for thinking about UI hierarchy. The chemistry metaphor helps teams communicate: “This button is an atom, this search form is a molecule (label + input + button), this header is an organism (logo molecule + navigation molecule + search molecule).” The power comes from seeing both the forest and the trees simultaneously: you can drill down from a page to its template to its organisms to its molecules to its atoms, or build up from atoms to pages. This prevents the common trap of designing one-off pages with no reusable components. The five levels (atoms, molecules, organisms, templates, pages) aren’t strict rules—some teams add ions (design tokens) or ecosystems (multi-page flows)—they’re a shared language for discussing modularity.
Atomic design is a methodology for creating interface design systems, inspired by chemistry and the natural world. It consists of five distinct stages that work together to create a hierarchical and deliberate approach to UI design.
The Five Levels:
<button>, <input>, <label>, <h1>Key Principles:
<!-- ===== ATOMS ===== -->
<!-- Basic building blocks that can't be broken down further -->
<!-- Atom: Button -->
<button class="btn btn-primary">Click me</button>
<button class="btn btn-secondary">Cancel</button>
<!-- Atom: Input -->
<input type="text" class="input" placeholder="Enter text">
<!-- Atom: Label -->
<label class="label">Email</label>
<!-- Atom: Icon -->
<svg class="icon icon-search">
<use href="#icon-search"></use>
</svg>
<!-- Atom: Heading -->
<h2 class="heading heading-lg">Welcome</h2>
<!-- ===== MOLECULES ===== -->
<!-- Simple groups of atoms functioning together -->
<!-- Molecule: Form Field (label + input + error) -->
<div class="form-field">
<label class="label" for="email">Email</label>
<input type="email" class="input" id="email">
<span class="error-message">Please enter a valid email</span>
</div>
<!-- Molecule: Search Form (label + input + button) -->
<form class="search-form">
<label class="label sr-only" for="search">Search</label>
<input type="search" class="input" id="search" placeholder="Search...">
<button type="submit" class="btn btn-primary">
<svg class="icon"><use href="#icon-search"></use></svg>
Search
</button>
</form>
<!-- Molecule: Card Header (avatar + name + timestamp) -->
<div class="card-header">
<img src="avatar.jpg" alt="User avatar" class="avatar">
<div class="user-info">
<h3 class="heading heading-sm">Alice Johnson</h3>
<time class="timestamp">2 hours ago</time>
</div>
</div>
<!-- ===== ORGANISMS ===== -->
<!-- Complex components made of molecules and atoms -->
<!-- Organism: Site Header (logo + navigation + search) -->
<header class="site-header">
<!-- Logo molecule (image + text) -->
<div class="logo">
<img src="logo.svg" alt="Company Logo" class="logo-image">
<span class="logo-text">MyApp</span>
</div>
<!-- Navigation molecule (list of links) -->
<nav class="main-nav">
<a href="/home" class="nav-link">Home</a>
<a href="/products" class="nav-link">Products</a>
<a href="/about" class="nav-link">About</a>
</nav>
<!-- Search molecule -->
<form class="search-form">
<input type="search" class="input" placeholder="Search...">
<button type="submit" class="btn btn-primary">Search</button>
</form>
</header>
<!-- Organism: Product Card -->
<article class="product-card">
<!-- Image atom -->
<img src="product.jpg" alt="Product name" class="product-image">
<!-- Card content molecules -->
<div class="product-info">
<h3 class="heading heading-md">Product Name</h3>
<p class="product-description">Product description text here.</p>
<!-- Price molecule (label + price) -->
<div class="product-price">
<span class="price-label">Price:</span>
<span class="price-value">$99.99</span>
</div>
<!-- Button atom -->
<button class="btn btn-primary">Add to Cart</button>
</div>
</article>
// ===== DESIGN TOKENS (Sub-atomic level) =====
// Values that feed into atoms
const designTokens = {
colors: {
primary: '#0066cc',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545',
gray100: '#f8f9fa',
gray900: '#212529'
},
spacing: {
xs: '0.25rem', // 4px
sm: '0.5rem', // 8px
md: '1rem', // 16px
lg: '1.5rem', // 24px
xl: '2rem' // 32px
},
typography: {
fontFamily: {
base: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto',
heading: 'Georgia, serif'
},
fontSize: {
sm: '0.875rem', // 14px
base: '1rem', // 16px
lg: '1.125rem', // 18px
xl: '1.5rem', // 24px
'2xl': '2rem' // 32px
},
fontWeight: {
normal: 400,
medium: 500,
bold: 700
}
},
borderRadius: {
sm: '0.25rem',
md: '0.5rem',
lg: '1rem',
full: '9999px'
}
};
// ===== ATOMS =====
// React component examples
import React from 'react';
// Atom: Button
function Button({ variant = 'primary', size = 'md', children, ...props }) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
{...props}>
{children}
</button>
);
}
// Atom: Input
function Input({ type = 'text', ...props }) {
return (
<input
type={type}
className="input"
{...props}
/>
);
}
// Atom: Label
function Label({ htmlFor, children }) {
return (
<label htmlFor={htmlFor} className="label">
{children}
</label>
);
}
// Atom: Heading
function Heading({ level = 2, size = 'md', children }) {
const Tag = `h${level}`;
return <Tag className={`heading heading-${size}`}>{children}</Tag>;
}
// ===== MOLECULES =====
// Molecule: Form Field (label + input + error)
function FormField({ label, id, error, ...inputProps }) {
return (
<div className="form-field">
<Label htmlFor={id}>{label}</Label>
<Input id={id} aria-invalid={!!error} {...inputProps} />
{error && <span className="error-message" role="alert">{error}</span>}
</div>
);
}
// Molecule: Search Form
function SearchForm({ onSearch, placeholder = 'Search...' }) {
const [query, setQuery] = React.useState('');
const handleSubmit = (e) => {
e.preventDefault();
onSearch(query);
};
return (
<form className="search-form" onSubmit={handleSubmit}>
<Label htmlFor="search" className="sr-only">Search</Label>
<Input
type="search"
id="search"
placeholder={placeholder}
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Button type="submit" variant="primary">Search</Button>
</form>
);
}
// Molecule: User Badge (avatar + name)
function UserBadge({ user }) {
return (
<div className="user-badge">
<img src={user.avatar} alt="" className="avatar" />
<span className="user-name">{user.name}</span>
</div>
);
}
// ===== ORGANISMS =====
// Organism: Site Header
function SiteHeader({ user, onSearch }) {
return (
<header className="site-header">
{/* Logo molecule */}
<div className="logo">
<img src="/logo.svg" alt="MyApp" className="logo-image" />
<span className="logo-text">MyApp</span>
</div>
{/* Navigation */}
<nav className="main-nav">
<a href="/home" className="nav-link">Home</a>
<a href="/products" className="nav-link">Products</a>
<a href="/about" className="nav-link">About</a>
</nav>
{/* Search molecule */}
<SearchForm onSearch={onSearch} />
{/* User molecule */}
<UserBadge user={user} />
</header>
);
}
// Organism: Product Grid
function ProductGrid({ products }) {
return (
<div className="product-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// Organism: Product Card
function ProductCard({ product }) {
return (
<article className="product-card">
<img
src={product.image}
alt={product.name}
className="product-image"
/>
<div className="product-info">
<Heading level={3} size="md">{product.name}</Heading>
<p className="product-description">{product.description}</p>
<div className="product-price">
<span className="price-label">Price:</span>
<span className="price-value">${product.price}</span>
</div>
<Button variant="primary" onClick={() => addToCart(product)}>
Add to Cart
</Button>
</div>
</article>
);
}
// ===== TEMPLATES =====
// Template: E-commerce Homepage Layout
function HomepageTemplate({ header, hero, featuredProducts, footer }) {
return (
<div className="homepage-template">
{header}
<main>
<section className="hero-section">
{hero}
</section>
<section className="featured-products-section">
<Heading level={2} size="xl">Featured Products</Heading>
{featuredProducts}
</section>
</main>
{footer}
</div>
);
}
// Template: Dashboard Layout
function DashboardTemplate({ sidebar, mainContent, widgets }) {
return (
<div className="dashboard-template">
<aside className="dashboard-sidebar">
{sidebar}
</aside>
<main className="dashboard-main">
<div className="dashboard-widgets">
{widgets}
</div>
<div className="dashboard-content">
{mainContent}
</div>
</main>
</div>
);
}
// ===== PAGES =====
// Page: Actual Homepage with real content
function Homepage() {
const user = { name: 'Alice', avatar: '/alice.jpg' };
const products = [
{ id: 1, name: 'Laptop', description: 'Powerful laptop', price: 999, image: '/laptop.jpg' },
{ id: 2, name: 'Mouse', description: 'Wireless mouse', price: 29, image: '/mouse.jpg' }
];
return (
<HomepageTemplate
header={<SiteHeader user={user} onSearch={query => console.log(query)} />}
hero={
<div className="hero">
<Heading level={1} size="2xl">Welcome to MyApp</Heading>
<p>Find the best products</p>
<Button variant="primary" size="lg">Shop Now</Button>
</div>
}
featuredProducts={<ProductGrid products={products} />}
footer={<footer className="site-footer">© 2024 MyApp</footer>}
/>
);
}
<!-- ===== ATOMS ===== -->
<!-- Button.vue -->
<template>
<button
:class="['btn', `btn-${variant}`, `btn-${size}`]"
:type="type"
@click="$emit('click', $event)">
<slot />
</button>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
defineProps({
variant: { type: String, default: 'primary' },
size: { type: String, default: 'md' },
type: { type: String, default: 'button' }
});
defineEmits(['click']);
</script>
<!-- Input.vue -->
<template>
<input
:type="type"
:value="modelValue"
:placeholder="placeholder"
:aria-invalid="invalid"
class="input"
@input="$emit('update:modelValue', $event.target.value)">
</template>
<script setup>
defineProps({
type: { type: String, default: 'text' },
modelValue: String,
placeholder: String,
invalid: Boolean
});
defineEmits(['update:modelValue']);
</script>
<!-- ===== MOLECULES ===== -->
<!-- FormField.vue -->
<template>
<div class="form-field">
<label :for="id" class="label"></label>
<Input
:id="id"
v-model="inputValue"
:type="type"
:placeholder="placeholder"
:invalid="!!error"
/>
<span v-if="error" class="error-message" role="alert">
</span>
</div>
</template>
<script setup>
import { computed } from 'vue';
import Input from './Input.vue';
const props = defineProps({
label: String,
id: String,
type: String,
placeholder: String,
modelValue: String,
error: String
});
const emit = defineEmits(['update:modelValue']);
const inputValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
});
</script>
<!-- SearchForm.vue -->
<template>
<form class="search-form" @submit.prevent="handleSearch">
<Input
v-model="query"
type="search"
:placeholder="placeholder"
/>
<Button type="submit" variant="primary">
Search
</Button>
</form>
</template>
<script setup>
import { ref } from 'vue';
import Input from './Input.vue';
import Button from './Button.vue';
defineProps({
placeholder: { type: String, default: 'Search...' }
});
const emit = defineEmits(['search']);
const query = ref('');
function handleSearch() {
emit('search', query.value);
}
</script>
<!-- ===== ORGANISMS ===== -->
<!-- ProductCard.vue -->
<template>
<article class="product-card">
<img
:src="product.image"
:alt="product.name"
class="product-image">
<div class="product-info">
<h3 class="heading heading-md"></h3>
<p class="product-description"></p>
<div class="product-price">
<span class="price-label">Price:</span>
<span class="price-value">$</span>
</div>
<Button variant="primary" @click="$emit('add-to-cart', product)">
Add to Cart
</Button>
</div>
</article>
</template>
<script setup>
import Button from './Button.vue';
defineProps({
product: {
type: Object,
required: true
}
});
defineEmits(['add-to-cart']);
</script>
<!-- SiteHeader.vue -->
<template>
<header class="site-header">
<div class="logo">
<img src="/logo.svg" alt="MyApp" class="logo-image">
<span class="logo-text">MyApp</span>
</div>
<nav class="main-nav">
<a href="/home" class="nav-link">Home</a>
<a href="/products" class="nav-link">Products</a>
<a href="/about" class="nav-link">About</a>
</nav>
<SearchForm @search="handleSearch" />
<div class="user-badge">
<img :src="user.avatar" alt="" class="avatar">
<span class="user-name"></span>
</div>
</header>
</template>
<script setup>
import SearchForm from './SearchForm.vue';
defineProps({
user: {
type: Object,
required: true
}
});
const emit = defineEmits(['search']);
function handleSearch(query) {
emit('search', query);
}
</script>
<!-- ===== TEMPLATES ===== -->
<!-- HomepageTemplate.vue -->
<template>
<div class="homepage-template">
<slot name="header" />
<main>
<section class="hero-section">
<slot name="hero" />
</section>
<section class="featured-products-section">
<h2 class="heading heading-xl">Featured Products</h2>
<slot name="featured-products" />
</section>
</main>
<slot name="footer" />
</div>
</template>
<!-- ===== PAGES ===== -->
<!-- HomePage.vue -->
<template>
<HomepageTemplate>
<template #header>
<SiteHeader :user="user" @search="handleSearch" />
</template>
<template #hero>
<div class="hero">
<h1 class="heading heading-2xl">Welcome to MyApp</h1>
<p>Find the best products</p>
<Button variant="primary" size="lg">Shop Now</Button>
</div>
</template>
<template #featured-products>
<div class="product-grid">
<ProductCard
v-for="product in products"
:key="product.id"
:product="product"
@add-to-cart="addToCart"
/>
</div>
</template>
<template #footer>
<footer class="site-footer">© 2024 MyApp</footer>
</template>
</HomepageTemplate>
</template>
<script setup>
import { ref } from 'vue';
import HomepageTemplate from './HomepageTemplate.vue';
import SiteHeader from './SiteHeader.vue';
import ProductCard from './ProductCard.vue';
import Button from './Button.vue';
const user = ref({ name: 'Alice', avatar: '/alice.jpg' });
const products = ref([
{ id: 1, name: 'Laptop', description: 'Powerful laptop', price: 999, image: '/laptop.jpg' },
{ id: 2, name: 'Mouse', description: 'Wireless mouse', price: 29, image: '/mouse.jpg' }
]);
function handleSearch(query) {
console.log('Search:', query);
}
function addToCart(product) {
console.log('Add to cart:', product);
}
</script>
Mistake: Building molecules directly without reusable atoms.
// ❌ BAD: One-off search form with no reusable atoms
function SearchForm() {
return (
<form>
{/* Hardcoded styles, no reusability */}
<input
type="search"
style={{ padding: '8px', border: '1px solid #ccc' }}
placeholder="Search..."
/>
<button style={{ background: '#0066cc', color: 'white', padding: '8px 16px' }}>
Search
</button>
</form>
);
}
// ✅ GOOD: Build from reusable atoms
function Button({ children, ...props }) {
return <button className="btn btn-primary" {...props}>{children}</button>;
}
function Input({ type = 'text', ...props }) {
return <input className="input" type={type} {...props} />;
}
function SearchForm() {
return (
<form className="search-form">
<Input type="search" placeholder="Search..." />
<Button type="submit">Search</Button>
</form>
);
}
// Now Button and Input can be reused elsewhere
Why it matters: Without atoms, you’ll duplicate code and have inconsistent styling across your app.
Mistake: Creating overly complex molecules or too-simple organisms.
// ❌ BAD: Molecule doing too much (this is an organism)
function UserProfileMolecule({ user }) {
return (
<div className="user-profile">
{/* Too complex for a molecule */}
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio}</p>
<ul>
{user.posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
<button>Follow</button>
<button>Message</button>
</div>
);
}
// ✅ GOOD: Break into proper hierarchy
// Molecule: User Badge (simple, single purpose)
function UserBadge({ user }) {
return (
<div className="user-badge">
<img src={user.avatar} alt="" className="avatar" />
<span>{user.name}</span>
</div>
);
}
// Molecule: Post List Item
function PostListItem({ post }) {
return (
<li className="post-item">
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
);
}
// Organism: User Profile Card (combines molecules)
function UserProfileCard({ user }) {
return (
<div className="user-profile-card">
<UserBadge user={user} />
<p className="user-bio">{user.bio}</p>
<ul className="post-list">
{user.posts.map(post => (
<PostListItem key={post.id} post={post} />
))}
</ul>
<div className="actions">
<Button>Follow</Button>
<Button variant="secondary">Message</Button>
</div>
</div>
);
}
Why it matters: Molecules should be simple and focused. Complex components should be organisms.
Mistake: Building components that only work on one page.
// ❌ BAD: HomePage-specific component
function HomePageProductGrid({ products }) {
return (
<div className="homepage-product-grid">
<h2>Featured on Homepage</h2> {/* Hardcoded text */}
<div className="grid-4-col"> {/* Hardcoded 4 columns */}
{products.slice(0, 4).map(product => ( /* Hardcoded limit */
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<span>${product.price}</span>
</div>
))}
</div>
</div>
);
}
// Can only be used on homepage!
// ✅ GOOD: Reusable organism with props
function ProductGrid({ products, columns = 3, heading }) {
return (
<div className="product-grid">
{heading && <h2 className="grid-heading">{heading}</h2>}
<div className={`grid-${columns}-col`}>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}
// Now works on any page
function HomePage() {
return <ProductGrid products={featured} columns={4} heading="Featured Products" />;
}
function CategoryPage() {
return <ProductGrid products={allProducts} columns={3} heading="All Products" />;
}
Why it matters: Reusable organisms reduce code duplication and ensure consistency across pages.
{product.description}
{/* Atom */} <PriceDisplay price={product.price} /> {/* Molecule */} <Button onClick={() => addToCart(product)}>Add to Cart</Button> {/* Atom */}By {post.author} on {post.date}
Brief content...
Extremely long content that tests how the layout handles lots of text...