はじめに#
Reactアプリケーションを開発していると、コンポーネントの数が増えるにつれて「このコンポーネントはどこに配置すべきか」「どの粒度でコンポーネントを分割すべきか」という課題に直面します。こうした問題を解決するために注目されているのが、Brad Frost氏が提唱したAtomic DesignというUI設計手法です。
Atomic Designは、UIを化学の原子(Atom)から分子(Molecule)、有機体(Organism)へと構築されるように、5つの階層で体系化する方法論です。Reactのコンポーネントベースのアーキテクチャと非常に相性が良く、多くのフロントエンドプロジェクトで採用されています。
本記事では、Atomic Designの基本概念からReactプロジェクトへの具体的な導入方法まで、実践的な知識を解説します。
本記事を読むことで、以下のことができるようになります。
- Atomic Designの5つの階層構造(Atoms, Molecules, Organisms, Templates, Pages)の理解
- 各階層へのコンポーネント分類基準の習得
- Reactプロジェクトにおけるディレクトリ構成のベストプラクティス
- Atomic Design導入時のメリット・デメリットの把握
実行環境・前提条件#
必要な環境#
- Node.js 20.x以上
- React 18以降
- TypeScript 5.x
- Viteで作成したReactプロジェクト(推奨)
- VS Code(推奨)
前提知識#
- Reactの基本的なコンポーネント設計の理解
- TypeScriptの基本的な型定義の知識
- Propsによるコンポーネント間のデータ受け渡し
対象読者#
- Reactプロジェクトのコンポーネント設計に課題を感じている開発者
- デザインシステムの構築に興味がある方
- コンポーネントの再利用性を高めたい方
Atomic Designとは#
Atomic Designは、2013年にBrad Frost氏が提唱したUIデザイン手法です。化学における物質の構成(原子→分子→有機体)にインスパイアされ、UIを5つの階層で体系的に構築することを提案しています。
Atomic Designの5つの階層#
Atomic Designでは、UIを以下の5つの階層に分類します。
graph TD
A[Atoms<br/>最小単位のUI要素] --> B[Molecules<br/>Atomsの組み合わせ]
B --> C[Organisms<br/>独立した機能を持つセクション]
C --> D[Templates<br/>ページのレイアウト構造]
D --> E[Pages<br/>実際のコンテンツを含む最終UI]
| 階層 |
説明 |
具体例 |
| Atoms |
これ以上分解できない最小単位のUI要素 |
ボタン、ラベル、入力フィールド、アイコン |
| Molecules |
複数のAtomsを組み合わせた小さなコンポーネント |
検索フォーム、ラベル付き入力フィールド |
| Organisms |
Molecules/Atomsで構成される独立したUIセクション |
ヘッダー、フッター、商品カード一覧 |
| Templates |
ページのレイアウト構造(コンテンツの骨格) |
ホームページテンプレート、記事詳細テンプレート |
| Pages |
Templatesに実際のコンテンツを流し込んだ最終形態 |
実データを表示するホームページ |
化学のアナロジーを超えて#
Atomic Designの「原子」「分子」という命名は、チーム内でのコミュニケーションを円滑にするためのメタファーです。重要なのは、UIを階層的に構築するという思想であり、命名にこだわる必要はありません。実際のプロジェクトでは、チームが理解しやすい独自の命名規則を採用することも推奨されています。
Atomsの設計#
Atomsは、Atomic Designにおける最も基本的な構成要素です。HTMLの基本タグに相当するレベルの、これ以上分解できないUI要素を定義します。
Atomsの特徴#
- 単一の責務を持つ
- 他のコンポーネントに依存しない
- 高い再利用性を持つ
- デザイントークン(色、サイズ、フォント)を適用する
Atomsの実装例#
ボタンコンポーネントを例に、Atomsの実装を見てみます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
// src/components/atoms/Button/Button.tsx
import { type ComponentProps, type ReactNode } from 'react';
import styles from './Button.module.css';
type ButtonVariant = 'primary' | 'secondary' | 'danger';
type ButtonSize = 'small' | 'medium' | 'large';
type ButtonProps = {
children: ReactNode;
variant?: ButtonVariant;
size?: ButtonSize;
disabled?: boolean;
} & Omit<ComponentProps<'button'>, 'className'>;
export const Button = ({
children,
variant = 'primary',
size = 'medium',
disabled = false,
...props
}: ButtonProps) => {
const className = `${styles.button} ${styles[variant]} ${styles[size]}`;
return (
<button className={className} disabled={disabled} {...props}>
{children}
</button>
);
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
/* src/components/atoms/Button/Button.module.css */
.button {
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
transition: opacity 0.2s;
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.primary {
background-color: #3b82f6;
color: white;
}
.secondary {
background-color: #e5e7eb;
color: #374151;
}
.danger {
background-color: #ef4444;
color: white;
}
.small {
padding: 6px 12px;
font-size: 12px;
}
.medium {
padding: 10px 20px;
font-size: 14px;
}
.large {
padding: 14px 28px;
font-size: 16px;
}
|
他のAtomsの例#
1
2
3
4
5
6
7
8
9
10
11
|
// src/components/atoms/Input/Input.tsx
import { type ComponentProps, forwardRef } from 'react';
import styles from './Input.module.css';
type InputProps = Omit<ComponentProps<'input'>, 'className'>;
export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} className={styles.input} {...props} />;
});
Input.displayName = 'Input';
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// src/components/atoms/Label/Label.tsx
import { type ComponentProps, type ReactNode } from 'react';
import styles from './Label.module.css';
type LabelProps = {
children: ReactNode;
required?: boolean;
} & ComponentProps<'label'>;
export const Label = ({ children, required = false, ...props }: LabelProps) => {
return (
<label className={styles.label} {...props}>
{children}
{required && <span className={styles.required}>*</span>}
</label>
);
};
|
Moleculesの設計#
Moleculesは、複数のAtomsを組み合わせて作成する、比較的シンプルなUIコンポーネントです。単一責任の原則に従い、1つの明確な機能を持ちます。
Moleculesの特徴#
- 2つ以上のAtomsで構成される
- 特定の機能を実現する
- 再利用可能な単位である
- ビジネスロジックを含まない
Moleculesの実装例#
検索フォームをMoleculeとして実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
// src/components/molecules/SearchForm/SearchForm.tsx
import { type FormEvent, useState } from 'react';
import { Button } from '@/components/atoms/Button/Button';
import { Input } from '@/components/atoms/Input/Input';
import styles from './SearchForm.module.css';
type SearchFormProps = {
onSearch: (query: string) => void;
placeholder?: string;
};
export const SearchForm = ({
onSearch,
placeholder = '検索キーワードを入力',
}: SearchFormProps) => {
const [query, setQuery] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (query.trim()) {
onSearch(query.trim());
}
};
return (
<form className={styles.form} onSubmit={handleSubmit}>
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={placeholder}
/>
<Button type="submit">検索</Button>
</form>
);
};
|
1
2
3
4
5
6
|
/* src/components/molecules/SearchForm/SearchForm.module.css */
.form {
display: flex;
gap: 8px;
align-items: center;
}
|
ラベル付き入力フィールドの例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
// src/components/molecules/FormField/FormField.tsx
import { type ComponentProps, type ReactNode } from 'react';
import { Input } from '@/components/atoms/Input/Input';
import { Label } from '@/components/atoms/Label/Label';
import styles from './FormField.module.css';
type FormFieldProps = {
label: string;
id: string;
required?: boolean;
error?: string;
} & ComponentProps<'input'>;
export const FormField = ({
label,
id,
required = false,
error,
...inputProps
}: FormFieldProps) => {
return (
<div className={styles.field}>
<Label htmlFor={id} required={required}>
{label}
</Label>
<Input id={id} aria-invalid={!!error} {...inputProps} />
{error && <span className={styles.error}>{error}</span>}
</div>
);
};
|
Organismsの設計#
Organismsは、Atoms・Molecules、または他のOrganismsを組み合わせて構成される、比較的複雑なUIセクションです。独立した機能を持ち、ページの明確なセクションを形成します。
Organismsの特徴#
- 複数のMolecules/Atomsで構成される
- 独立したUIセクションとして機能する
- ビジネスドメインを反映することが多い
- 特定のコンテキストに依存する場合がある
Organismsの実装例#
ヘッダーコンポーネントをOrganismとして実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
// src/components/organisms/Header/Header.tsx
import { SearchForm } from '@/components/molecules/SearchForm/SearchForm';
import { Button } from '@/components/atoms/Button/Button';
import styles from './Header.module.css';
type HeaderProps = {
siteName: string;
onSearch: (query: string) => void;
onLogin: () => void;
isLoggedIn: boolean;
userName?: string;
};
export const Header = ({
siteName,
onSearch,
onLogin,
isLoggedIn,
userName,
}: HeaderProps) => {
return (
<header className={styles.header}>
<div className={styles.logo}>
<h1>{siteName}</h1>
</div>
<nav className={styles.nav}>
<a href="/">ホーム</a>
<a href="/products">商品一覧</a>
<a href="/about">会社情報</a>
</nav>
<div className={styles.actions}>
<SearchForm onSearch={onSearch} />
{isLoggedIn ? (
<span className={styles.userName}>{userName}さん</span>
) : (
<Button onClick={onLogin} variant="secondary">
ログイン
</Button>
)}
</div>
</header>
);
};
|
商品カード一覧の例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
// src/components/organisms/ProductGrid/ProductGrid.tsx
import { ProductCard } from '@/components/molecules/ProductCard/ProductCard';
import styles from './ProductGrid.module.css';
type Product = {
id: string;
name: string;
price: number;
imageUrl: string;
description: string;
};
type ProductGridProps = {
products: Product[];
onProductClick: (id: string) => void;
};
export const ProductGrid = ({ products, onProductClick }: ProductGridProps) => {
if (products.length === 0) {
return <p className={styles.empty}>商品がありません</p>;
}
return (
<div className={styles.grid}>
{products.map((product) => (
<ProductCard
key={product.id}
name={product.name}
price={product.price}
imageUrl={product.imageUrl}
description={product.description}
onClick={() => onProductClick(product.id)}
/>
))}
</div>
);
};
|
Templatesの設計#
Templatesは、Organismsやその他のコンポーネントを配置するレイアウト構造を定義します。実際のコンテンツではなく、ページの骨格(スケルトン)を表現します。
Templatesの特徴#
- ページのレイアウト構造を定義する
- コンテンツの配置位置を示す
- 実際のデータは含まない
- 複数のページで再利用される
Templatesの実装例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
// src/components/templates/MainTemplate/MainTemplate.tsx
import { type ReactNode } from 'react';
import styles from './MainTemplate.module.css';
type MainTemplateProps = {
header: ReactNode;
sidebar?: ReactNode;
main: ReactNode;
footer: ReactNode;
};
export const MainTemplate = ({
header,
sidebar,
main,
footer,
}: MainTemplateProps) => {
return (
<div className={styles.layout}>
<div className={styles.header}>{header}</div>
<div className={styles.body}>
{sidebar && <aside className={styles.sidebar}>{sidebar}</aside>}
<main className={styles.main}>{main}</main>
</div>
<div className={styles.footer}>{footer}</div>
</div>
);
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
/* src/components/templates/MainTemplate/MainTemplate.module.css */
.layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.header {
position: sticky;
top: 0;
z-index: 100;
}
.body {
display: flex;
flex: 1;
}
.sidebar {
width: 250px;
flex-shrink: 0;
}
.main {
flex: 1;
padding: 24px;
}
.footer {
margin-top: auto;
}
|
Pagesの設計#
Pagesは、Templatesに実際のデータを流し込んだ最終的なUIです。ルーティングと対応し、ユーザーが実際に目にする画面を表現します。
Pagesの特徴#
- Templatesを使用してレイアウトを決定する
- 実際のデータやコンテンツを含む
- データフェッチやグローバルな状態管理と連携する
- ルーティングのエントリーポイントとなる
Pagesの実装例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
// src/components/pages/HomePage/HomePage.tsx
import { useState, useEffect } from 'react';
import { MainTemplate } from '@/components/templates/MainTemplate/MainTemplate';
import { Header } from '@/components/organisms/Header/Header';
import { Footer } from '@/components/organisms/Footer/Footer';
import { ProductGrid } from '@/components/organisms/ProductGrid/ProductGrid';
import { useNavigate } from 'react-router-dom';
export const HomePage = () => {
const [products, setProducts] = useState([]);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const navigate = useNavigate();
useEffect(() => {
// 商品データのフェッチ
const fetchProducts = async () => {
const response = await fetch('/api/products');
const data = await response.json();
setProducts(data);
};
fetchProducts();
}, []);
const handleSearch = (query: string) => {
navigate(`/search?q=${encodeURIComponent(query)}`);
};
const handleProductClick = (id: string) => {
navigate(`/products/${id}`);
};
return (
<MainTemplate
header={
<Header
siteName="ECサイト"
onSearch={handleSearch}
onLogin={() => navigate('/login')}
isLoggedIn={isLoggedIn}
/>
}
main={
<ProductGrid products={products} onProductClick={handleProductClick} />
}
footer={<Footer />}
/>
);
};
|
Atomic Designのディレクトリ構成#
Atomic DesignをReactプロジェクトに導入する際の推奨ディレクトリ構成を紹介します。
基本的なディレクトリ構成#
src/
├── components/
│ ├── atoms/
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.module.css
│ │ │ ├── Button.test.tsx
│ │ │ └── index.ts
│ │ ├── Input/
│ │ ├── Label/
│ │ └── Icon/
│ ├── molecules/
│ │ ├── SearchForm/
│ │ ├── FormField/
│ │ └── ProductCard/
│ ├── organisms/
│ │ ├── Header/
│ │ ├── Footer/
│ │ └── ProductGrid/
│ ├── templates/
│ │ ├── MainTemplate/
│ │ └── AuthTemplate/
│ └── pages/
│ ├── HomePage/
│ ├── ProductPage/
│ └── LoginPage/
├── hooks/
├── utils/
└── App.tsx
インデックスファイルによるエクスポート#
各階層にインデックスファイルを作成し、インポートを簡潔にします。
1
2
3
4
5
|
// src/components/atoms/index.ts
export { Button } from './Button/Button';
export { Input } from './Input/Input';
export { Label } from './Label/Label';
export { Icon } from './Icon/Icon';
|
1
2
3
4
|
// src/components/molecules/index.ts
export { SearchForm } from './SearchForm/SearchForm';
export { FormField } from './FormField/FormField';
export { ProductCard } from './ProductCard/ProductCard';
|
これにより、インポートが簡潔になります。
1
2
3
|
// 使用例
import { Button, Input, Label } from '@/components/atoms';
import { SearchForm, FormField } from '@/components/molecules';
|
Atomic Designにおけるコンポーネント分類の判断基準#
コンポーネントをどの階層に分類するか迷うことは多いです。以下の判断基準を参考にしてください。
分類のフローチャート#
flowchart TD
A[新しいコンポーネント] --> B{これ以上<br/>分解可能か?}
B -->|No| C[Atoms]
B -->|Yes| D{ビジネスドメインに<br/>依存するか?}
D -->|No| E{複数のAtomsの<br/>組み合わせか?}
D -->|Yes| F[Organisms]
E -->|Yes| G[Molecules]
E -->|No| C
F --> H{ページの<br/>レイアウト構造か?}
H -->|Yes| I[Templates]
H -->|No| F具体的な判断例#
| コンポーネント |
分類 |
理由 |
| ボタン |
Atoms |
HTMLのbutton要素に対応する最小単位 |
| アイコン |
Atoms |
単一のSVGまたは画像要素 |
| ラベル付き入力 |
Molecules |
Label + Inputの組み合わせ |
| 検索バー |
Molecules |
Input + Buttonの組み合わせ |
| ヘッダー |
Organisms |
ロゴ、ナビゲーション、検索バーなど複数のMoleculesを含む |
| 商品カード一覧 |
Organisms |
ビジネスドメイン(商品)に依存 |
| 2カラムレイアウト |
Templates |
ページのレイアウト構造を定義 |
| トップページ |
Pages |
実際のデータを表示する最終UI |
Atomic Designのメリット#
再利用性の向上#
Atomic Designでは、Atomsを基盤として上位の階層を構築するため、コンポーネントの再利用性が自然と高まります。一度作成したButtonやInputは、プロジェクト全体で一貫して使用できます。
一貫したデザインシステム#
階層構造により、デザインの一貫性が保たれます。Atomsレベルでデザイントークン(色、サイズ、間隔など)を定義しておけば、上位の階層でも自動的に一貫したスタイルが適用されます。
スケーラビリティ#
プロジェクトが大規模化しても、コンポーネントの配置場所が明確なため、新しい開発者がプロジェクトに参加しやすくなります。「このコンポーネントはMoleculesに配置する」といった共通認識が生まれます。
デザイナーとの協業#
Atomic Designはもともとデザインシステム構築のための方法論です。デザイナーがAtomic Designに基づいてデザインを作成している場合、開発者との間でスムーズなコミュニケーションが可能になります。
Atomic Designのデメリット#
分類の曖昧さ#
「このコンポーネントはMoleculesかOrganismsか」という判断が難しい場合があります。チーム内で明確な基準を設けないと、分類がばらつく可能性があります。
過度な分割のリスク#
Atomic Designを厳格に適用しすぎると、不要に細かいコンポーネント分割が発生し、かえって複雑さが増す場合があります。プロジェクトの規模に応じた適切な粒度を見極める必要があります。
学習コスト#
チームメンバー全員がAtomic Designの概念を理解する必要があります。新しいメンバーへのオンボーディングにおいて、追加の説明が必要になります。
ディレクトリ構造の深さ#
階層が増えることで、ファイルパスが長くなり、インポート文が煩雑になる可能性があります。パスエイリアス(@/components/atomsなど)の設定で軽減できます。
Atomic Designと他の設計手法との比較#
Feature Sliced Design(FSD)との違い#
Feature Sliced Design(FSD)は「ビジネスドメイン」を軸にコードを分割するアーキテクチャです。Atomic Designが「UIの粒度」で分類するのに対し、FSDは「機能・責務」で分類します。
| 観点 |
Atomic Design |
Feature Sliced Design |
| 分類軸 |
UIの粒度(Atoms → Pages) |
責務・ドメイン(Layers + Slices) |
| 主な用途 |
デザインシステム構築 |
アプリケーションアーキテクチャ |
| 依存関係 |
下位から上位への一方向 |
上位から下位への一方向 |
| 適用範囲 |
UIコンポーネント |
プロジェクト全体 |
両者は排他的ではなく、併用も可能です。FSDのshared/ui層にAtomic Design的な階層構造を導入するアプローチもあります。
コロケーションとの併用#
Atomic Designは「コンポーネントの種類別」にファイルを配置しますが、コロケーション(関連ファイルを近くに配置する方針)と組み合わせることで、より保守しやすい構成になります。
src/components/atoms/Button/
├── Button.tsx # コンポーネント本体
├── Button.module.css # スタイル
├── Button.test.tsx # テスト
├── Button.stories.tsx # Storybook
└── index.ts # エクスポート
Atomic Design導入のベストプラクティス#
段階的な導入#
既存プロジェクトにAtomic Designを導入する場合は、一度にすべてを変更するのではなく、新規コンポーネントから段階的に適用することをおすすめします。
分類基準のドキュメント化#
チーム内で「どのような基準でAtomsとMoleculesを分けるか」をドキュメント化しておくと、分類のブレを防げます。
Storybookとの連携#
Atomic DesignとStorybookは非常に相性が良いです。各階層のコンポーネントをStorybookでカタログ化することで、デザインシステムとして活用できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// src/components/atoms/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Atoms/Button',
component: Button,
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
},
size: {
control: 'select',
options: ['small', 'medium', 'large'],
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
children: 'ボタン',
variant: 'primary',
},
};
export const Secondary: Story = {
args: {
children: 'ボタン',
variant: 'secondary',
},
};
|
柔軟な運用#
Atomic Designは厳格なルールではなく、ガイドラインです。プロジェクトの特性に合わせて、4階層(Templates省略)や、独自の命名規則を採用することも検討してください。
まとめ#
Atomic Designは、UIを5つの階層(Atoms、Molecules、Organisms、Templates、Pages)で体系化する設計手法です。Reactのコンポーネントベースのアーキテクチャと相性が良く、以下のメリットがあります。
- コンポーネントの再利用性向上
- 一貫したデザインシステムの構築
- 大規模プロジェクトでのスケーラビリティ
- デザイナーとの円滑なコミュニケーション
一方で、分類の曖昧さや学習コストなどのデメリットもあるため、チーム内で明確な基準を設け、プロジェクトの規模に応じた適切な粒度で運用することが重要です。
Atomic Designは、デザインシステムを構築する際の強力なフレームワークとして、多くのフロントエンドプロジェクトで活用されています。本記事で紹介した実装例やベストプラクティスを参考に、ぜひプロジェクトへの導入を検討してみてください。
参考リンク#