JavaScript and React course Β· Module 15: Patterns and Architecture
Atomic Design in React - Building a station from atoms
In this lesson8
The orbital station has grown to twenty screens. Each of them has a button, a progress bar and a status badge, but every team wrote them in its own way: here the bar is six pixels high, there it is eight, and the red alarm comes in three different shades. When Commander Nova asks for all critical badges to look the same, you hunt for them in forty files. The problem is not any single component, but the lack of a plan that says which parts the interface is made of. Atomic Design gives you that plan.
The five levels of Atomic Design
Atomic Design is a way of thinking about an interface that Brad Frost described in 2013 and later expanded in his book "Atomic Design". It borrows its names from nature: atoms combine into molecules, and molecules into organisms. On the station it looks like this:
- Atoms: the smallest parts that make no sense to split further, like a bolt, an indicator light or a switch. In React these are
Button,Input,BadgeorProgressBar. - Molecules: a few atoms that serve one function together, like a switch with an indicator light and a label. For example,
SearchBaris anInputand aButton. - Organisms: self-contained sections of the interface built from molecules and atoms, like the whole systems panel on the bridge.
- Templates: a page layout with slots but without real data, like a deck plan that marks where each panel will stand.
- Pages: a template filled with real data, the deck on the day of the mission.
The order of the levels is always the same, from the smallest to the largest: atoms, molecules, organisms, templates, pages. Frost stresses, though, that this is a model and not a step-by-step process: you can work on several levels at the same time.
Atoms - the smallest parts
An atom does not know where it will be used. It receives data through props and always looks the same. The simplest one is Badge, a colored label with a short text:
1// atoms/Badge.jsx
2function Badge({ children, color = 'blue' }) {
3 return <span className={`badge badge-${color}`}>{children}</span>;
4}The color comes from outside as the name of a variant, not as a color code, so you set the actual shade in one place: in the styles of the badge-red class. When the alarm has to change its shade, you fix one line of CSS, not forty files.
ProgressBar is more interesting. Visually it is two rectangles, but a screen reader has to know that it is a progress bar, what it measures and what its value is. That is why the bar gets the progressbar role, a description in aria-label and three numeric attributes:
1// atoms/ProgressBar.jsx
2function ProgressBar({ value, label }) {
3 return (
4 <div
5 className="progress"
6 role="progressbar"
7 aria-label={label}
8 aria-valuenow={value}
9 aria-valuemin={0}
10 aria-valuemax={100}
11 >
12 <div className="progress-fill" style={{ width: `${value}%` }} />
13 </div>
14 );
15}aria-valuenow gives the current value, while aria-valuemin and aria-valuemax give its range. The width of the fill is the same number written as a percentage, so the look and the description for the screen reader never drift apart. The browser also has a ready-made <progress> element. We build our own bar with the progressbar role when we need full control over its look, and then we have to describe it ourselves.
Molecules - atoms with one job
A molecule combines a few atoms into something that already has a meaning. The classic example is a search box: the Input atom, a text field in the station's style, and the Button atom together serve one purpose, searching:
1// molecules/SearchBar.jsx
2function SearchBar({ onSearch }) {
3 const [query, setQuery] = useState('');
4
5 return (
6 <div className="search-bar">
7 <Input value={query} onChange={(e) => setQuery(e.target.value)} />
8 <Button onClick={() => onSearch(query)}>Search</Button>
9 </div>
10 );
11}SearchBar remembers the text being typed, but it does not know what it is searching for: it hands the query over through onSearch. UserCard (an avatar, a name and a role badge) is built the same way, and so is the StatusRow status row, which will soon become part of a bigger panel:
1// molecules/StatusRow.jsx
2function StatusRow({ label, value, color }) {
3 return (
4 <div className="status-row">
5 <span className="status-label">{label}</span>
6 <ProgressBar value={value} label={label} />
7 <Badge color={color}>{value}%</Badge>
8 </div>
9 );
10}StatusRow adds no styling beyond the layout: the look of the bar and the badge belongs to the atoms. When you improve ProgressBar, it improves in every row on the station.
Organisms - whole sections of the interface
An organism is a section you can put on a screen and understand right away what it is for. The SystemsPanel panel receives a list of systems and draws one StatusRow for each of them. It decides only one thing itself: the statusColors map says which badge color matches which status:
1// organisms/SystemsPanel.jsx
2const statusColors = { ok: 'green', warning: 'orange', critical: 'red' };
3
4function SystemsPanel({ systems }) {
5 return (
6 <section className="systems-panel">
7 <h2>Ship systems</h2>
8 {systems.map(system => (
9 <StatusRow
10 key={system.id}
11 label={system.name}
12 value={system.level}
13 color={statusColors[system.status]}
14 />
15 ))}
16 </section>
17 );
18}The map translates the language of the data (ok, warning, critical) into the language of the atoms (green, orange, red). The atoms do not know system statuses, and the data does not know colors. You build CrewPanel the same way: a SearchBar above a list of UserCard cards.
Templates and pages
A template is a layout without content. DashboardTemplate only says where the header, the side panel and the main part stand, and it receives the content through slots, like the layouts from the first lesson of the module:
1// templates/DashboardTemplate.jsx
2function DashboardTemplate({ header, sidebar, main }) {
3 return (
4 <div className="dashboard">
5 <header className="dash-header">{header}</header>
6 <aside className="dash-sidebar">{sidebar}</aside>
7 <main className="dash-main">{main}</main>
8 </div>
9 );
10}The MissionDashboard page is the last level: it takes the template and puts organisms with real data into it. The useFetch hook fetches the data, the NavigationBar organism stands in the header, and while the data is loading, the page shows a Spinner:
1// pages/MissionDashboard.jsx
2function MissionDashboard() {
3 const systems = useFetch('/api/systems');
4 const crew = useFetch('/api/crew');
5
6 if (systems.loading || crew.loading) return <Spinner />;
7
8 return (
9 <DashboardTemplate
10 header={<NavigationBar />}
11 sidebar={<CrewPanel crew={crew.data} />}
12 main={<SystemsPanel systems={systems.data} />}
13 />
14 );
15}In this layout only the page knows where the data comes from. In the final project you will see that an organism may also reach for a data hook, but never an atom or a molecule. The same template with other organisms gives you a crew screen, and the same SystemsPanel will show test data in a component catalog.
Folder structure
The levels of Atomic Design are easiest to see in the file tree. Each level gets its own folder in src/components, and the folder name tells you right away how big the component inside is:
1src/
2 components/
3 atoms/
4 Button.jsx
5 Input.jsx
6 Badge.jsx
7 ProgressBar.jsx
8 molecules/
9 SearchBar.jsx
10 StatusRow.jsx
11 UserCard.jsx
12 organisms/
13 SystemsPanel.jsx
14 CrewPanel.jsx
15 NavigationBar.jsx
16 templates/
17 DashboardTemplate.jsx
18 pages/
19 MissionDashboard.jsxThe import path repeats this tree. Every file exports its component with export default, so you import the button from components/atoms/Button and the status row from components/molecules/StatusRow:
1import Button from 'components/atoms/Button';
2import StatusRow from 'components/molecules/StatusRow';Such a path starts from the src folder, so the project has to know about it: in Vite you add an alias in resolve.alias, and in Next.js you set baseUrl or paths in the jsconfig.json file. Without that you write a relative path, for example ../atoms/Button.
The price of order
Atomic Design is not free. The main trade-off goes like this: you gain more reusable components, but you pay with more files and a more complex folder structure. A small form split into five levels means a dozen files instead of one, and the team has to agree whether a given component is still a molecule or already an organism. In a large application with dozens of screens this price pays off quickly: you fix the button in one place, a new person on the team knows right away where to look for the status row, and you test atoms in isolation. In an application with three views, a simple split into components/ and pages/ is enough.
Refactoring a monolith step by step
You rarely start from scratch. More often you get one huge panel component and want to put it in order. Go from the bottom up, in this order:
- Identify the repeating UI elements: the same buttons, bars and badges in many places.
- Extract atoms from them, for example
Button,InputandBadge. - Combine the atoms into molecules, such as
SearchBar,UserCardorStatusRow. - Build organisms from the molecules, such as
CrewPanelandSystemsPanel. - Finally, create the template and the page that arrange the organisms on the screen.
After each step the application should look and work exactly as before. If something changed, you know right away which step broke it.
My advice: do not create five empty folders on day one. Start with the atoms that really repeat, and add the next levels when the code itself asks for them. When you do not know where to put a component, ask whether it makes sense on its own: an atom works anywhere, a molecule needs atoms, and an organism is already a whole section of the screen. In the next lesson you will meet the Provider Pattern and state machines, which come in handy when organisms start sharing state.
Remember: you assemble the station from small, proven parts instead of carving every screen from scratch.
Code for this lesson: App.jsx
1import React, { useState } from 'react';
2
3// ============ ATOMS ============
4
5function Button({ variant = 'primary', children, ...props }) {
6 return (
7 <button className={`btn btn-${variant}`} {...props}>
8 {children}
9 </button>
10 );
11}
12
13function Input(props) {
14 return <input className="input" {...props} />;
15}
16
17function Badge({ children, color = 'blue' }) {
18 return <span className={`badge badge-${color}`}>{children}</span>;
19}
20
21function ProgressBar({ value, label }) {
22 return (
23 <div
24 className="progress"
25 role="progressbar"
26 aria-label={label}
27 aria-valuenow={value}
28 aria-valuemin={0}
29 aria-valuemax={100}
30 >
31 <div className="progress-fill" style={{ width: `${value}%` }} />
32 </div>
33 );
34}
35
36function Avatar({ name }) {
37 return <span className="avatar" aria-hidden="true">{name[0]}</span>;
38}
39
40// ============ MOLECULES ============
41
42function SearchBar({ onSearch, placeholder = 'Search...' }) {
43 const [query, setQuery] = useState('');
44 return (
45 <div className="search-bar">
46 <Input
47 placeholder={placeholder}
48 aria-label={placeholder}
49 value={query}
50 onChange={(e) => setQuery(e.target.value)}
51 />
52 <Button onClick={() => onSearch(query)}>Search</Button>
53 </div>
54 );
55}
56
57function StatusRow({ label, value, color }) {
58 return (
59 <div className="status-row">
60 <span className="status-label">{label}</span>
61 <ProgressBar value={value} label={label} />
62 <Badge color={color}>{value}%</Badge>
63 </div>
64 );
65}
66
67// Role values stay codes in the data, the crew sees readable labels
68const roleLabels = { captain: 'Captain', medic: 'Medic', engineer: 'Engineer', navigator: 'Navigator' };
69
70function UserCard({ user, onClick }) {
71 return (
72 <button className="user-card" onClick={onClick}>
73 <Avatar name={user.name} />
74 <span className="user-info">
75 <span className="user-name">{user.name}</span>
76 <Badge color={user.role === 'captain' ? 'gold' : 'blue'}>{roleLabels[user.role]}</Badge>
77 </span>
78 </button>
79 );
80}
81
82// ============ ORGANISMS ============
83
84const statusColors = { ok: 'green', warning: 'orange', critical: 'red' };
85
86function SystemsPanel({ systems }) {
87 return (
88 <section className="systems-panel">
89 <h2>Ship systems</h2>
90 {systems.map(system => (
91 <StatusRow
92 key={system.id}
93 label={system.name}
94 value={system.level}
95 color={statusColors[system.status]}
96 />
97 ))}
98 </section>
99 );
100}
101
102function CrewPanel({ crew, onSelect }) {
103 const [query, setQuery] = useState('');
104 // The list is computed during render, no copy of the props in state
105 const visible = crew.filter(m => m.name.toLowerCase().includes(query.toLowerCase()));
106
107 return (
108 <section className="crew-panel">
109 <h2>Crew</h2>
110 <SearchBar onSearch={setQuery} placeholder="Search the crew..." />
111 {visible.map(member => (
112 <UserCard key={member.id} user={member} onClick={() => onSelect(member)} />
113 ))}
114 {visible.length === 0 && <p className="empty">No results</p>}
115 </section>
116 );
117}
118
119// ============ TEMPLATE: layout only, no data ============
120
121function DashboardTemplate({ header, sidebar, main }) {
122 return (
123 <div className="dashboard">
124 <header className="dash-header">{header}</header>
125 <aside className="dash-sidebar">{sidebar}</aside>
126 <main className="dash-main">{main}</main>
127 </div>
128 );
129}
130
131// ============ PAGE: the template filled with data ============
132
133const crew = [
134 { id: 1, name: 'Commander Nova', role: 'captain' },
135 { id: 2, name: 'Dr. Stellar', role: 'medic' },
136 { id: 3, name: 'Astro', role: 'engineer' },
137 { id: 4, name: 'Ra', role: 'navigator' },
138];
139
140const systemNames = { engines: 'Engines', shields: 'Shields', lifeSupport: 'Life support', comms: 'Comms' };
141
142const levelToStatus = (level) => (level >= 70 ? 'ok' : level >= 40 ? 'warning' : 'critical');
143
144export default function App() {
145 const [levels, setLevels] = useState({ engines: 95, shields: 42, lifeSupport: 100, comms: 18 });
146 const [selected, setSelected] = useState(null);
147
148 const systems = Object.keys(levels).map(id => ({
149 id,
150 name: systemNames[id],
151 level: levels[id],
152 status: levelToStatus(levels[id]),
153 }));
154
155 const boostShields = () =>
156 setLevels(prev => ({ ...prev, shields: Math.min(prev.shields + 20, 100) }));
157
158 return (
159 <DashboardTemplate
160 header={<h1>Atomic Design in React</h1>}
161 sidebar={<CrewPanel crew={crew} onSelect={setSelected} />}
162 main={
163 <>
164 <SystemsPanel systems={systems} />
165 <Button variant="secondary" onClick={boostShields} disabled={levels.shields === 100}>
166 Boost shields by 20%
167 </Button>
168 {selected && (
169 <div className="detail-box">
170 <h3>Selected: {selected.name}</h3>
171 <p>Role: {roleLabels[selected.role]}</p>
172 </div>
173 )}
174 <div className="hierarchy">
175 <h2>Levels on this screen</h2>
176 <div className="level"><Badge color="blue">Atoms</Badge><span>Button, Input, Badge, ProgressBar, Avatar</span></div>
177 <div className="level"><Badge color="green">Molecules</Badge><span>SearchBar, StatusRow, UserCard</span></div>
178 <div className="level"><Badge color="orange">Organisms</Badge><span>SystemsPanel, CrewPanel</span></div>
179 <div className="level"><Badge color="purple">Template</Badge><span>DashboardTemplate</span></div>
180 <div className="level"><Badge color="gold">Page</Badge><span>This App component</span></div>
181 </div>
182 </>
183 }
184 />
185 );
186}Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. At which Atomic Design level is a SearchBar component that combines Input and Button?
2. What is the main trade-off of applying Atomic Design in large React projects?
Hands-on tasks in the game
- Code editor
Build the ship systems panel from atoms, a molecule and an organism. The ProgressBar atom limits the value to the range 0-100 (the percent constant): ___BLANK1___ is the value of the aria-valuenow attribute, and ___BLANK2___ is the width of the fill in percent as a string, for example '45%'. The StatusRow molecule combines atoms: ___BLANK3___ is the ProgressBar atom with the value value and the label label (a screen reader takes the name of the bar from it), placed between the name and the badge. The SystemsPanel organism creates a StatusRow for every system: ___BLANK4___ is the badge colour read from STATUS_COLORS by the system status (online, degraded, offline).
- Vertical ordering
Arrange the Atomic Design levels from smallest to largest:
- Click in order
Click the elements of the import path for the Button atom in the correct order: