Atomic Design
Atomic Designとは
・Brad Frost氏が考案したデザインシステム
・画面要素を5段階に分け、組み合わせることでUIを実現
・コンポーネント化された要素が画面を構成しているという考え方
・React、Vue用というわけではない
・モダンJavaScriptと相性がいい
5段階のコンポーネント
ATOMS → MOLECULES → ORGANISMS → TEMPLATES → PAGES
原子 → 分子 → 有機体 → テンプレート → ページ
ATOMS
Atoms 原子
最も小さくそれ以上分解できない要素
・ボタン
・ツイート入力テキストボックス
・アイコン 等
MOLECULES
Molecules 分子
Atomの組み合わせで意味を持つデザインパターン
・アイコン + メニュー名
・プロフィール画像 + テキストボックス
・アイコンセット 等
ORGANISMS
Organisms 有機体
AtomやMoleculeの組み合わせで構成される単体である程度の意味を持つ要素群
・ツイート入力エリア
・サイドメニュー
・1つのツイートエリア 等
TEMPLATES
Templates テンプレート
ページのレイアウトのみを表現する要素(実際のデータは持たない)
・サイドメニュー
・ツイートエリア
・トピックエリア等のレイアウト情報 等
PAGES
Pages ページ
最終的に表示される1画面
・ページ遷移毎に表示される各画面
Atom 作成
$ mkdir -p src/components/atoms/button
$ touch src/components/atoms/button/BaseButton.jsx $ touch src/components/atoms/button/PrimaryButton.jsx $ touch src/components/atoms/button/SecondaryButton.jsx
./components/atoms/button/BaseButton.jsx
import styled from "styled-components"; export const BaseButton = styled.button` color: #fff; padding: 6px 24px; border: none; border-radius: 9999px; outline: none; &:hover { cursor: pointer; opacity: 0.8; } `;
./components/atoms/button/PrimaryButton.jsx
import styled from "styled-components"; import { BaseButton } from "./BaseButton"; export const PrimaryButton = (props) => { const { children } = props; return <SButton>{children}</SButton>; }; const SButton = styled(BaseButton)` background-color: #40514e; `;
./components/atoms/button/SecondaryButton.jsx
import styled from "styled-components"; import { BaseButton } from "./BaseButton"; export const SecondaryButton = (props) => { const { children } = props; return <SButton>{children}</SButton>; }; const SButton = styled(BaseButton)` background-color: #11999e; `;
Molecules 作成
$ mkdir -p src/components/molecules $ mkdir -p src/components/atoms/input
$ touch src/components/molecules/SearchInput.jsx $ touch src/components/atoms/input/Input.jsx
./components/atoms/input/Input.jsx
import styled from "styled-components"; export const Input = (props) => { const { placeholder = "" } = props; return <SInput type="text" placeholder={placeholder} />; }; const SInput = styled.input` padding: 8px 16px; border: solid #ddd 1px; border-radius: 9999px; outline: none; `;
./components/molecules/SearchInput.jsx
import styled from "styled-components"; import { PrimaryButton } from "../atoms/button/PrimaryButton"; import { Input } from "../atoms/input/Input"; export const SearchInput = () => { return ( <SContainer> <Input placeholder="検索条件を入力" /> <SButtonWrapper> <PrimaryButton>検索</PrimaryButton> </SButtonWrapper> </SContainer> ); }; const SContainer = styled.div` display: flex; align-items: center; `; const SButtonWrapper = styled.div` padding-left: 8px; `;
Organisms 作成
$ mkdir -p src/components/organisms/user $ mkdir -p src/components/atoms/card $ mkdir -p src/components/molecules/user
$ touch src/components/organisms/user/UserCard.jsx $ touch src/components/atoms/card/Card.jsx $ touch src/components/molecules/user/UserIconWithName.jsx
./components/atoms/card/Card.jsx
import styled from "styled-components"; export const Card = (props) => { const { children } = props; return <SCard>{children}</SCard>; }; const SCard = styled.div` background-color: #fff; box-shadow: #ddd 0px 0px 4px 2px; border-radius: 8px; padding: 16px; `;
./components/organisms/user/UserCard.jsx
import styled from "styled-components"; import { Card } from "../../atoms/card/Card"; import { UserIconWithName } from "../../molecules/user/UserIconWithName"; export const UserCard = (props) => { const { user } = props; return ( <Card> <UserIconWithName image={user.image} name={user.name} /> <SDL> <dt>メール</dt> <dd>{user.email}</dd> <dt>TEL</dt> <dd>{user.phone}</dd> <dt>会社名</dt> <dd>{user.company.name}</dd> <dt>WEB</dt> <dd>{user.website}</dd> </SDL> </Card> ); }; const SDL = styled.dl` text-align: left; margin-bottom: 0px; dt { float: left; } dd { padding-left: 32px; padding-bottom: 8px; overflow-wrap: break-word; } `;
/components/molecules/user/UserIconWithName.jsx
import styled from "styled-components"; export const UserIconWithName = (props) => { const { image, name } = props; return ( <SContainer> <SImg height={160} width={160} src={image} alt={name} /> <SName>{name}</SName> </SContainer> ); }; const SContainer = styled.div` text-align: center; `; const SImg = styled.img` border-radius: 50%; `; const SName = styled.p` font-size: 18px; font-weight: bold; margin: 0; color: #40514e; `;
./App.jsx
import { PrimaryButton } from "./components/atoms/button/PrimaryButton"; import { SecondaryButton } from "./components/atoms/button/SecondaryButton"; import { SearchInput } from "./components/molecules/SearchInput"; import { UserCard } from "./components/organisms/user/UserCard"; import "./styles.css"; export default function App() { const user = { name: "Bob", image: "https://source.unsplash.com/2l0CWTpcChI", email: "12345@example.com", phone: "090-1111-2222", company: { name: "テスト株式会社" }, website: "https://google.com" }; return ( <div className="App"> <PrimaryButton>テスト</PrimaryButton> <SecondaryButton>検索</SecondaryButton> <br /> <SearchInput /> <UserCard user={user} /> </div> ); }
Template 作成
$ mkdir -p src/components/templates $ mkdir -p src/components/atoms/layout
$ touch src/components/templates/DefaultLayout.jsx $ touch src/components/templates/HeaderOnly.jsx $ touch src/components/atoms/layout/Header.jsx $ touch src/components/atoms/layout/Footer.jsx
./components/templates/HeaderOnly.jsx
import { Link } from "react-router-dom"; import styled from "styled-components"; export const Header = () => { return ( <SHeader> <SLink to="/">HOME</SLink> <SLink to="/users">USERS</SLink> </SHeader> ); }; const SHeader = styled.header` background-color: #11999e; color: #fff; text-align: center; padding: 8px 0px; `; const SLink = styled(Link)` margin: 0 8px; `;
./components/templates/DefaultLayout.jsx
import { Footer } from "../atoms/layout/Footer"; import { Header } from "../atoms/layout/Header"; export const DefaultLayout = (props) => { const { children } = props; return ( <> <Header /> {children} <Footer /> </> ); };
/components/atoms/layout/Header.jsx
import { Link } from "react-router-dom"; import styled from "styled-components"; export const Header = () => { return ( <SHeader> <SLink to="/">HOME</SLink> <SLink to="/users">USERS</SLink> </SHeader> ); }; const SHeader = styled.header` background-color: #11999e; color: #fff; text-align: center; padding: 8px 0px; `; const SLink = styled(Link)` margin: 0 8px; `;
./components/atoms/layout/Footer.jsx
import { Link } from "react-router-dom"; import styled from "styled-components"; export const Footer = () => { return <SFooter>© 2021 test Inc.</SFooter>; }; const SFooter = styled.footer` background-color: #11999e; color: #fff; text-align: center; padding: 8px 0px; position: fixed; bottom: 0; width: 100%; `;
./App.jsx
import { BrowserRouter } from "react-router-dom"; import { PrimaryButton } from "./components/atoms/button/PrimaryButton"; import { SecondaryButton } from "./components/atoms/button/SecondaryButton"; import { SearchInput } from "./components/molecules/SearchInput"; import { UserCard } from "./components/organisms/user/UserCard"; import { DefaultLayout } from "./components/templates/DefaultLayout"; import { HeaderOnly } from "./components/templates/HeaderOnly"; import "./styles.css"; export default function App() { const user = { name: "Bob", image: "https://source.unsplash.com/2l0CWTpcChI", email: "12345@example.com", phone: "090-1111-2222", company: { name: "テスト株式会社" }, website: "https://google.com" }; return ( <BrowserRouter> <DefaultLayout> <PrimaryButton>テスト</PrimaryButton> <SecondaryButton>検索</SecondaryButton> <br /> <SearchInput /> <UserCard user={user} /> </DefaultLayout> </BrowserRouter> ); }
Pages 作成
$ mkdir -p src/components/pages $ mkdir -p src/router
$ touch src/components/pages/Top.jsx $ touch src/components/page/Users.jsx $ touch src/components/atoms/layout/Header.jsx $ touch src/components/atoms/layout/Footer.jsx $ touch src/router/Router.jsx
./components/pages/Top.jsx
import styled from "styled-components"; export const Top = () => { return ( <SContainer> <h2>TOPページです</h2> </SContainer> ); }; const SContainer = styled.div` text-align: center; `;
./components/page/Users.jsx
import styled from "styled-components"; import { SearchInput } from "../molecules/SearchInput"; import { UserCard } from "../organisms/user/UserCard"; const users = [...Array(10).keys()].map((val) => { return { id: val, name: `Bob${val}`, image: "https://source.unsplash.com/2l0CWTpcChI", email: "12345@example.com", phone: "090-1111-2222", company: { name: "テスト株式会社" }, website: "https://google.com" }; }); export const Users = () => { return ( <SContainer> <h2>ユーザー一覧です</h2> <SearchInput /> <SUserArea> {users.map((user) => ( <UserCard key={user.id} user={user} /> ))} </SUserArea> </SContainer> ); }; const SContainer = styled.div` text-align: center; flex-direction: column; align-items: center; padding: 24px; `; const SUserArea = styled.div` padding-top: 40px; width: 100%; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-gap: 20px; `;
./components/atoms/layout/Header.jsx
import { Link } from "react-router-dom"; import styled from "styled-components"; export const Header = () => { return ( <SHeader> <SLink to="/">HOME</SLink> <SLink to="/users">USERS</SLink> </SHeader> ); }; const SHeader = styled.header` background-color: #11999e; color: #fff; text-align: center; padding: 8px 0px; `; const SLink = styled(Link)` margin: 0 8px; `;
./components/atoms/layout/Footer.jsx
import styled from "styled-components"; export const Footer = () => { return <SFooter>© 2021 test Inc.</SFooter>; }; const SFooter = styled.footer` background-color: #11999e; color: #fff; text-align: center; padding: 8px 0px; position: fixed; bottom: 0; width: 100%; `;
./router/Router.jsx
import { BrowserRouter, Switch, Route } from "react-router-dom"; import { Top } from "../components/pages/Top"; import { Users } from "../components/pages/Users"; import { DefaultLayout } from "../components/templates/DefaultLayout"; import { HeaderOnly } from "../components/templates/HeaderOnly"; export const Router = () => { return ( <BrowserRouter> <Switch> <Route exact path="/"> <DefaultLayout> <Top /> </DefaultLayout> </Route> <Route path="/users"> <HeaderOnly> <Users /> </HeaderOnly> </Route> </Switch> </BrowserRouter> ); };
./App.jsx
import { Router } from "./router/Router"; import "./styles.css"; export default function App() { return <Router />; }