UI
TypeScript
bem
React
AtomicDesign

Atomic Designの小さな"Atom"をガチで真剣に考え抜いて実装する話


Atomを作ったことありますか?

以前、「Atomic Designってデザイナーには難しくない!?という話」という記事を書きまして、大変反響があり、今でも時折いいねをいただいていて大変嬉しく思っています!

この反響からも、やっぱり世間的にこの手の話題はみんな一生懸命悩みながら、わからないながらに作っているんだろうなーとなんとなく感じています。

そんな私の記事ですが、前はAtomsからPagesまでの全体の話をしました。

それゆえに、実際に運用できるレベルの、超具体的なAtom自体の実装についてはそこまで触れませんでした。

なので今回は、堅牢で使いまわしやすく運用しやすいAtomをガチで作ったらどうなるのかを記事にしてみたいと思います。


「Atoms」とはどんな存在なのだろう?

車輪の再発明」という言葉がありますね。

しれっと皮肉を言っているこの言葉ですが、確かになにか家具を作るときに、わざわざ「釘」から作るのかと言われると、確かに作らないな…となりますよね。

UIの世界でその「釘」に相当するものは、Atomic Designでは「Atoms」にあたるのだろうと自分は思います。

例えば、「ボタン」というAtomがあったとしたら、その「ボタン」は家具でいうところの「釘」のようにUIを作る上で当たり前に使われるべきものであるべきです。


それでも「車輪」は作りたくなる

と言いつつも、結局「車輪の再発明」を行ってしまう人は多いはずです。

すでにあるそのボタンの、色、形状、文字のスタイルなどが必ずしも「良いUI」であるとは限りません

使い手がそのAtomレベルのUIを「使いづらい」と思ってしまったら、そのAtomは使い手によって改修をしたくてたまらなくなってしまうわけです。

もちろん、この「使いづらい」にもいろいろあると思います。

汎用性がなかったり、参照しづらかったり、選択しづらかったり、、、

結局のところ、その「車輪」を使ってみて、初めて「使いやすさ」に気づくわけです。

その「使いづらい」は、デザイン的な側面から見ても、コードの側面から見ても、原因はその「車輪」によって様々で、解決のアプローチの仕方はたくさんあるのだと思います。


コードで「最高の車輪」を目指してみる

ならばせめてでも、「コードに落とし込む」ときに、少しでも「車輪」が使いやすくなるように工夫できないだろうか?とFEの私は思うわけです。

なので、車輪を作る上で「楽」「堅」「隙」という3つをキーワードとして作り上げていきます。

「楽」

これは、「使い手」が迷うことなく使えるようにしていくことを意識します。

車輪を使う人が、その車輪を使うこと自体が苦にならないようにするにはどうしていくべきかを考えます。

「堅」

これは、「車輪」を自由に生み出せないようにすることを意識します。

そもそもの「再発明」自体が起こりにくくする仕組みを作るにはどうしたらいいかを考えます。

「隙」

これは、「逃げ道」を作り出すことを意識します。

どうしてもその車輪では解決できないときのための最終手段がある状態をどう生み出すかを考えます。

「楽」は、React

「堅」は、TypeScript/BEM

「隙」は、HTML/CSS

で生み出していきたいと思います!


Atoms相当のReact Componentを作成する

ではさっそく作っていきたいと思います。

今回は「ボタン」を例にして全力で作っていきます。

見た目は以下のような感じです。

image.png

シンプルですね。

(スクショ撮ったら色が薄くなっちゃいました…)

これをコードに落とし込むと以下のようになります。


button/index.tsx

import React, { FunctionComponent } from 'react';

import './style.css';

const Button: FunctionComponent = ({ children }) => (
<button className="button">{children}</button>
);

export default Button;



button/style.css

.button {

display: inline-block;
padding: 10px 20px;
background-color: #0b4;
border-radius: 5px;
color: #fff;
font-size: 13px;
line-height: 1.2;
appearance: none;
}

※resetに、normalize.cssを使用しています。

シンプルに書けました!

これを以下のように使います。


index.tsx

import React, { FunctionComponent } from 'react';

import Button from './button';

const App: FunctionComponent = () => (
<Button>投稿する</Button>
);

export default App;


今までであれば、ボタンのためのHTMLとCSSを都度書かなければなりませんでした。

例えbuttonのクラスが用意されていたり、Sassとかでbutton用のmixinが用意されていたりしたとしても、それに合わせたHTMLは書かなければなりません。

それが、Reactを用いることによって<Button>...</Button>と書くだけでボタンのUIが「楽」に実現できるようになっています!

これであれば、みんな自分で作らずにこっちの<Button>を使ってくれそうです!


パターンを作る

ボタンは、何もこの全体エメラルドグリーンのボタンだけとは限りません。

一つのサイトを作る上では、数パターンのボタンは存在するはずです。

なので、以下のボタンのデザインを追加して、それをButtonコンポーネントでも使えるようにします。

image.png

image.png

まずはCSSを以下のように変更します。

(class名の命名ルールはBEMを使用してます。)


button/style.css

.button {

display: inline-block;
padding: 9px 19px;
border: 1px solid;
background-color: #00d6b0;
border-radius: 5px;
box-sizing: border-box;
font-size: 13px;
line-height: 1.2;
appearance: none;
}

.button.button--green {
border-color: #00d6b0;
background-color: #00d6b0;
color: #fff;
}

.button.button--greenOutline {
border-color: #00d6d0;
background-color: #fff;
color: #00d6d0;
}

.button.button--grayOutline {
border-color: #808080;
background-color: #fff;
color: #333;
}


上記の修正したコードでは、Modifierのクラスを追加して、UIを切り替えられるようにしました。

ベースとなるUIのコードはすべてBlockのスタイルとして定義し、パターンの差分だけModifierのスタイルとして定義しています。

そうしたら、今度はComponent側を修正していきます。

せっかくModifierのclass名を用意したので、これが切り替えられるように工夫していきます。

class名自体は定数として管理していきたいため、ここでTypeScriptの記法の一つであるenumを使っていきたいと思います。


button/index.tsx

import React, { FunctionComponent } from 'react';

import './style.css';

enum ModifierClassNames {
GREEN = 'button--green',
GREEN_OUTLINE = 'button--greenOutline',
GRAY_OUTLINE = 'button--grayOutline'
}

const Button: FunctionComponent = ({ children }) => (
<button className={['button', ModifierClassNames.GREEN].join(' ')}>{children}</button>
);

export default Button;


ModifierClassNamesというenumを作成しました。

これをclassNameに対してBlock名の'button'ModifierClassNamesの緑ボタン用のModifierを参照して配列にし、.join(' ')することでclassName="button button--green"という形で出力されるように変更しました。

ただ、これだとbutton--greenしか参照されなくて、何のためにenumを定義したかわかりません。笑

さらに修正をいれて、外部からthemeというpropsを受け取って、そのpropsに応じてModifierが切り替わるように修正していきます。

その修正したパターンがこちらです。


button/index.tsx

import React, { FunctionComponent, ReactNode } from 'react';

import './style.css';

interface ButtonProps {
theme?: ButtonThemes;
children: ReactNode;
}

export enum ButtonThemes {
GREEN = 'GREEN',
GREEN_OUTLINE = 'GREEN_OUTLINE',
GRAY_OUTLINE = 'GRAY_OUTLINE'
}

enum ModifierClassNames {
GREEN = 'button--green',
GREEN_OUTLINE = 'button--greenOutline',
GRAY_OUTLINE = 'button--grayOutline'
}

const Button: FunctionComponent<ButtonProps> = ({ theme = ButtonThemes.GREEN, children }) => (
<button className={['button', ModifierClassNames[theme]].join(' ')}>{children}</button>
);

export default Button;


この修正で大きく変わったのはButtonThemesというenumができた点です。

そして、宣言通りthemeというpropsを用意しまして、さらにinterfaceを定義し、新しく作ったthemeの型を決めています

上記のように変えることで、Button Componentの緑のアウトラインのボタンを使う側は以下のようなコードを書くことになります。


index.tsx

import React, { FunctionComponent } from 'react';

import Button, { ButtonThemes } from './button';

const App: FunctionComponent = () => (
<Button theme={ButtonThemes.GREEN_OUTLINE}>投稿する</Button>
);

export default App;


ただ単にHTMLで作る場合には、CSSに合わせたModifier用のclass名を自分たちで入力する形になりますが、そうしてしまうと結局タイポ等でミスが生じてしまうため、ButtonThemesという中から自分が使いたいパターンを選択できる形にしています

image.png

(VSCodeを使っていれば、このようにenumをサジェストしてくれるのでUIが選びやすいです。)

ModifierClassNamesをexportしてclassNameというpropsに代入できるようにしてしまうと、例えばModifierClassNamesを組み合わせてUIを実現するAtomなんかがあった場合、何を組み合わせたらいいかわからなくて大混乱だー!となってしまいがちです。

今回はデザインとModifierが1対1なのでenumの定義がちょっと冗長には見えますが、ButtonThemesを作り、一つ選択すれば自分自身が実現したいUIを実現できるようにして、「楽」と「堅」を生み出しています。

また、ButtonThemesenumで作り、propsのthemeの型をButtonThemesにしているので、themeにも自由に文字列を入れられない形になっています。

もし、<Button theme="GREEN">と入れると以下のようなエラーが表示されます。

Type '"GREEN"' is not assignable to type 'ButtonThemes | undefined'.

ざっと訳すと、ButtonThemesかundefined以外割り当てられねーよ!ってことですね。

今入れている"GREEN"string型なので怒られているわけですね。

この状態だとコンパイルエラーが発生するため、Buildできなくなります。

このように、enumを活用してUIを選択しやすくしながら、そのenumをpropsの型定義となるinterfaceのプロパティの型として活用することで、堅牢なComponentを作成することができます!


UIの「微調整」をしやすくする

今までは、UIを定義した上でそれを選択しやすくしながらも、自由度を持たせないように堅牢にComponentを作成してきました。

しかしそれをしてしまうと、そのAtomを使うと、あと一歩でUIが実現できない!!という事態が発生して、結局、車輪の再発明をしたくなってしまう状況を生み出してしまいます。

そこで、最後にちょっとだけUIを編集できるようにする工夫を施していきます!

やることはシンプルです。

以下のように書き換えます。


button/index.tsx

import React, { FunctionComponent, ReactNode } from 'react';

import './style.css';

interface ButtonProps {
theme?: ButtonThemes;
className?: string;
children: ReactNode;
}

// ...省略

const Button: FunctionComponent<ButtonProps> = ({ theme = ButtonThemes.GREEN, className = '', children }) => (
<button className={['button', ModifierClassNames[theme], className].join(' ')}>{children}</button>
);

export default Button;


何をしたかというと、classNameを渡せるようにします!

classNameなんか渡せるようにしてしまったら、デザインをめちゃくちゃ上書きされてしまいそうです!!

確かにそのとおりなのですが、これにはちゃんと理由があります。


BEM記法を使って安全にする

今回はBEM記法を使っていますため、今回のclassNameを渡す修正をすると、BlockでありElementである状況を生み出すことができます。

もし、submitButtonという大きいBlockで使うことを想定した場合、吐き出されるHTMLとしてはこんな形になります。

<p class="submitButton">

<button class="button submitButton__main">投稿する</button>
</p>

つまり、一つのBlockを外側のElementとして扱えるようにできるということです

Elementとしてスタイルを微調整していく状況を作り出すことになります。

どんなにUIを堅牢にしたいと言えど、横幅を伸ばしたり、文字サイズを大きくしたりしたいはずです。

image.png

このぐらいはやらせてくれー!って感じですよね。

これをclassNameを渡さない状態で、Buttonを加工するコードを書くと以下のようになります。

(CSSの方が重要です。)


index.tsx

import React, { FunctionComponent } from 'react';

import Button, { ButtonThemes } from './button';
import 'style.css';

const App: FunctionComponent = () => (
<p className="submitButton">
<Button theme={ButtonThemes.GREEN}>投稿する</Button>
</p>
);

export default App;



style.css

.submitButton {

padding: 10px;
}

.submitButton .button {
width: 100%;
padding: 19px 29px;
font-size: 17px;
font-weight: bold;
}


見ていただいてわかる通り、外側のBlockのスタイルに他のBlockに関するスタイルが入り込んでしまいます。

つまり、BlockのことはBlockが決めれば良いのに、他のBlockにまで干渉していることになります

もう少し細かいことを言うと、.submitButton .buttonと書いているからまだ良いですが、.buttonと書いてしまったら、ページ全体のbuttonにスタイルを上書きしてしまう恐れがあります!

こんな状況になるぐらいなら、classNameを渡せるようにして、そのBlock同士が干渉し合わない状況を作った方がよっぽど良いです。

classNameを渡すと以下のように書けます。


index.tsx

import React, { FunctionComponent } from 'react';

import Button, { ButtonThemes } from './button';
import 'style.css';

const App: FunctionComponent = () => (
<p className="submitButton">
<Button className="submitButton__main" theme={ButtonThemes.GREEN}>投稿する</Button>
</p>
);

export default App;



style.css

.submitButton {

padding: 10px;
}

.submitButton .submitButton__main {
width: 100%;
padding: 19px 29px;
font-size: 17px;
font-weight: bold;
}


こうすることで、一つのBlockをElementとして扱うことができ、.buttonに対してスタイルをあてなくても良くなりました!

つまり、責任を負う範囲を明確に分けることができたわけです。

classNameを渡せてしまうと、何でもかんでも上書き出来てしまいそうで確かに不安ではありますが、逆に、どのBlockがそのUIのスタイルにしたのかがわかりやすくなり、崩れが生じた時も直す点が探しやすくなります


まとめ

こうして完成したButtonというAtomは以下のようなコードになりました。


button/index.tsx

import React, { FunctionComponent, ReactNode } from 'react';

import './style.css';

interface ButtonProps {
theme?: ButtonThemes;
className?: string;
children: ReactNode;
}

export enum ButtonThemes {
GREEN = 'GREEN',
GREEN_OUTLINE = 'GREEN_OUTLINE',
GRAY_OUTLINE = 'GRAY_OUTLINE'
}

enum ModifierClassNames {
GREEN = 'button--green',
GREEN_OUTLINE = 'button--greenOutline',
GRAY_OUTLINE = 'button--grayOutline'
}

const Button: FunctionComponent<ButtonProps> = ({ theme = ButtonThemes.GREEN, className = '', children }) => (
<button className={['button', ModifierClassNames[theme], className].join(' ')}>{children}</button>
);

export default Button;



button/style.css

.button {

display: inline-block;
padding: 9px 19px;
border: 1px solid;
background-color: #00d6b0;
border-radius: 5px;
box-sizing: border-box;
font-size: 13px;
line-height: 1.2;
appearance: none;
}

.button.button--green {
border-color: #00d6b0;
background-color: #00d6b0;
color: #fff;
}

.button.button--greenOutline {
border-color: #00d6d0;
background-color: #fff;
color: #00d6d0;
}

.button.button--grayOutline {
border-color: #808080;
background-color: #fff;
color: #333;
}


実際に、本当にこのコードが運用しやすいのか否かは、実際にそのUIを使ってデザインしてみないとわかりません。

ただ、「楽」「堅」「隙」の三つをよく考えて作ることで、そのサイトのデザインから、パーツを使う同僚のデザイナー、エンジニアのことまで考えぬいたプロダクトが作れるのではないかと思います。

Atomという小さい単位のComponentではありますが、いろいろと考え抜くと、作り方もいろいろ工夫できるものだなーと記事を書きながら思った次第です。

もっとこうした方が良いよ!という解決策等があれば、ぜひコメント欄までお願いします!


追記

あとになって気づきましたが、ModifierClassNamesは以下のように設定したほうが良かったかもしれません。

export enum ButtonThemes {

GREEN = 'GREEN',
GREEN_OUTLINE = 'GREEN_OUTLINE',
GRAY_OUTLINE = 'GRAY_OUTLINE'
}

const ModifierClassNames = {
ButtonThemes.GREEN: 'button--green',
ButtonThemes.GREEN_OUTLINE: 'button--greenOutline',
ButtonThemes.GRAY_OUTLINE: 'button--grayOutline'
}

※本来であれば[ButtonThemes.GREEN]: ~~というようにkeyに対して[]を入れなければいけませんが、コードブロックのレンダリングがうまくいかなかったので上記のように書きました。

そうすれば、ButtonThemesの値をそのままkeyに使えるので、よりミスがなくなったかなと思ったので、こちらの作り方でも良いかもですね!