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>&copy; 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>&copy; 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 />;
}