JavaScript
React
next.js
AtomicDesign
emotion

CSS in JS ライブラリ「emotion」で、Atomic Design風なコンポーネントを作ってみる


はじめに

emotion は、パフォーマンス良くスタイルを構築する事を念頭に設計された CSS-in-JSライブラリです。

emotion の使い方を少し体験すると同時に、コンポーネントごとに適用するにはどう書いていくかを探るために、 Atomic Design のカードの例を参考に、サンプルアプリケーションを作成しました。

この記事は、そのサンプルアプリケーション作成にあたって、現時点で気になったポイントや迷いがあるポイントなどを記載しています。

書き方は色々あると思いますが、 styled の書き方がしっくりきたので、それを使って書いています。


サンプルアプリケーション

コードも含めて以下に置いています。

Next.jsのSandboxをベースに書いています。

https://codesandbox.io/s/vnnl16yqny


Atoms

Atoms は、「もっとも基本的な要素」で、それ以上分解することができないものなので、 HTMLタグ1つで表現できるもの として考えます。

Atomsを利用するコンポーネントは props 経由でデータを Atoms のコンポーネントに渡します。

下記の card-image.js という Atoms のコンポーネントでは、画像のurlのみ props で渡してもらいます。(厳密にはdefault値は必要かもしれません)


atoms/card-image.js

import styled from "@emotion/styled";

const Img = styled.img({
width: "64px",
height: "64px",
borderRadius: "32px",
label: "a-card-image"
});

const CardImage = props => <Img src={props.imgSrc} />;

export default CardImage;


Atoms には marginを設定しない 方がよいのではないかと考えています。理由として、 marginは周りの要素と組み合わさって初めて発生するものなので、 Atoms 内で持つ事はないと思います。

一方で、周りの要素を組み合わさって発生するmarginについては、付与すべき対象はこの Atoms のコンポーネントになると思います。

そこで、 CSS props の機能を使って、以下のように実現します。


atoms/card-title.js

import styled from "@emotion/styled";

const Div = styled.div(
{
/* atoms は margin を持たない */
fontSize: "14px",
fontWeight: 600,
color: "rgba(0, 0, 0, 0.7)",
label: "a-card-title"
},
props => ({
/* molecules として組み合わさる時に margin が付与される */
margin: props.margin,
color: props.color
})
);

const CardTitle = props => <Div {...props.overrideStyle}>{props.title}</Div>;

export default CardTitle;


上記では、 props.overrideStyle からmarginのデータをもらい、上書きしています。

この機能を使えば、変更可能なプロパティを Atoms 側でコントロールできるので、上記の例だと、 「color は変えて良い」と定義できるかなと思います。


Molecules

Molecules は、 Atoms を組み合わせたもので、 独自の性質・機能を持つもの として考えます。

以下のように Atoms をimportしてきて、組み込みます。


molecules/card.js

import styled from "@emotion/styled";

import CardImage from "../atoms/card-image";
import CardTitle from "../atoms/card-title";
import CardDescription from "../atoms/card-description";

...

const Card = props => (
<Div {...props.overrideStyle.Card}>
<CardImage imgSrc={props.imgSrc} />
<DivCardInfo>
<CardTitle
overrideStyle={combineStyle(overrideStyleOfCardTitle, {
...props.overrideStyle.CardTitle
})}
title={props.title}
/>
<CardDescription
overrideStyle={combineStyle(overrideStyleOfCardDescription, {
...props.overrideStyle.CardDescription
})}
description={props.description}
/>
</DivCardInfo>
</Div>
);

export default Card;


今回のサンプルでは、上位のまとまりである Organisms と同様に悩んでいる部分ではあるのですが、 style を親のコンポーネントから来るものと組み合わせて子のコンポーネントに渡す必要があります。

そのマージ処理は今回以下のようにして作りました。


molecules/card.js

...

// 子コンポーネントの書き換え可能なstyleを上書きする
const overrideStyleOfCardTitle = {
margin: "4px 0 0 0"
};

// 子コンポーネントの書き換え可能なstyleを上書きする
const overrideStyleOfCardDescription = {
margin: "8px 0 0 0"
};

/*
親コンポーネントからのスタイル指定とmergeする方法
*/

const combineStyle = (thisStyle, parentStyle) => {
return Object.assign(thisStyle, parentStyle);
};

const Card = props => (
  ...
<CardTitle
overrideStyle={combineStyle(overrideStyleOfCardTitle, {
...props.overrideStyle.CardTitle
})}
title={props.title}
/>
 
...
);


上記では、 combineStyle() で、マージをしています。

マージの仕方は色々あると思っていて、どれを基本とするか、他にいい案がないかはまだ自分の中では固まっていないです。


Organisms

Organisms は、「原子・分子を組み合わせた比較的複雑な構造」で、 MolculesAtoms を組み合わせて作るまとまりとして考えます。

このサンプルでは、シンプルなカードのリストをこのレイヤーに配置しています。

基本的には Organisms と考える事は同じなのですが、このサンプルで1つハマった点がありました。

cssでよくある :first-of-type といった擬似クラスのスタイルについてどうやって表現するかです。

この擬似クラスの受け口を子のコンポーネントに持たせると、他のプロパティと同じ方法で表現できますが、

それを子コンポーネントが持つのはどうなんだろうというのが、自分の中で疑問になりました。

今回のサンプルでは、苦肉の策で以下のように実現させましたが、何か他にいい方法がないかは考えたいです。


organisms/card-list.js

...

// XXX: first-of-type の表現をpropsで渡すのがわからず、苦肉の策
// 回数に応じて適用するstyleを分岐させる。
const overrideStyleOfCard = (i, style) => {
const defaultStyle = {
margin: "8px 0 0 0"
};
const firstChildStyle = {
margin: "0"
};

return i == 0
? Object.assign(style.Card, firstChildStyle)
: Object.assign(style.Card, defaultStyle);
};

const CardList = ({ cardInfoList }) => (
<Div>
{cardInfoList.map((data, i) => {
overrideStyleOfCard(i, data.overrideStyle);
return <Card {...data} key={i} />;
})}
</Div>
);

...



Templates

Templates は、「インタフェースの骨格」で、UIコンテンツ構造にフォーカスしたものです。

これまでの Atoms から Organisms の作り方であれば不要なのではないかな?と思い、今回はこれに当たるコンポーネントは作りませんでした。


Pages

Pages は、実際のコンテンツを適用したもの。 今回のコンポーネント設計では一番上位にあたるもの と考えます。

そのため、ページの基本構造のDOMはここに記載していきます。


pages/index.js

import CardImage from "../atoms/card-image";

import CardTitle from "../atoms/card-title";
import CardDescription from "../atoms/card-description";
import Card from "../molecules/card";
import CardList from "../organisms/card-list";
import { H1, H2, H3 } from "../global-style/h";

// カードの情報
const redCardInfo = {
...
}

const greenCardInfo = {
...
}

const blueCardInfo = {
...
}

// カード情報リスト
const sampleCardInfoList = [redCardInfo, greenCardInfo, blueCardInfo];

export default () => (
<section>
<H1>Atomic Design Practice</H1>
<p>
このサンプルは、
<a href="https://uxdaystokyo.com/articles/glossary/atomic-design/">
https://uxdaystokyo.com/articles/glossary/atomic-design/
</a>
のカードの例を参考に組み立てています。
</p>
<div>
<H2>Atoms</H2>
<H3>CardImage</H3>
<CardImage imgSrc={blueCardInfo.imgSrc} />
<H3>CardTitle</H3>
<CardTitle title={blueCardInfo.title} />
<H3>CardDescription</H3>
<CardDescription description={blueCardInfo.description} />
</div>
<div>
<H2>Molecules</H2>
<H3>Card</H3>
<Card {...blueCardInfo} />
</div>
<div>
<H2>Organisms</H2>
<H3>Card List</H3>
<CardList cardInfoList={sampleCardInfoList} />
</div>
</section>
);


Next.js そのものをまだ勉強していないのですが、 pages ディレクトリ以下がルーティングの起点となっているようで、 偶然の一致かもしれませんが、相性はよさそうです。


共通スタイル

例えば、hXタグは共通したスタイルを持ちたいなというときは、 common 的な場所に書いておき、 import して使うのが良さそうかなと思っています。


global-style/h.js

import styled from "@emotion/styled";

// hX の共通 style
const HcommonStyle = {
color: "#eee",
padding: "8px",
borderRadius: "4px"
};

// h1 の style
export const H1 = styled.h1(
{ ...HcommonStyle },
{
fontSize: "24px",
background: "#222",
label: "g-h1"
}
);

// h2 の style
export const H2 = styled.h2(
{ ...HcommonStyle },
{
fontSize: "20px",
background: "#555",
label: "g-h2"
}
);

// h3 の style
export const H3 = styled.h3(
{ ...HcommonStyle },
{
fontSize: "14px",
fontWeight: "500",
background: "#999",
label: "g-h3"
}
);



pages/index.js

...

import { H1, H2, H3 } from "../global-style/h";
...

export default () => (
<section>
<H1>Atomic Design Practice</H1>
...


ディレクトリ構造的にどこに置くかは少し悩みどころですが・・・。


おわりに

今回は触れてませんが、 emotion は記法がいくつか選択できるので、初めてCSS-in-JSを使ってみた自分でも、基本的な使い方はすぐに慣れました。

課題として、 Atomic Design 風にコンポーネント分割していく際に、style の上書きをどうするかや、擬似クラスの表現の仕方があるのと、 Next.js で実際にアプリケーションを作っていくと、どういった壁が出てくるかが未知数です。

一方で、これまではcss(scss)をFLOCSSで管理したりしていましたが、結局JS側のコンポーネントと乖離が出たりして、運用が長く続けば続くほど、辛い感じになってきている印象です。

これをCSS in JSで実装し、かつコンポーネントは Atomic Design の考え方をベースにしてルール化しておくと、チーム開発時に混乱が起きる確率が減るのではないかなと思っています。

まだまだ足りない観点やルールがありそうなので、これをベースにさらに作ってみて、新たな気づきがあれば、アップデートもしくは新規で書き起こそうと思います。


おまけ

実は上記サンプルは2つ目で、ボツにした1つ目は以下。

https://codesandbox.io/s/136z47n693

overrideするstyleは全て上部でDOMを使って吸収するというやり方が大きく違います。どちらが良いかは好みかもしれませんが、対応しきれないケースがでるかなと思い、ボツにしました。