はじめに
自分は普段フロントエンドエンジニアとして、React Reduxなアーキテクチャのアプリを作ることが多いのですが、stylingにstyled-componentsを導入しています。今回は、styled-componentsでのCSS設計について書いてみたいと思います。
styled-componentsとは
JSでstyleを記述するCSS in JSのライブラリで、2019年8月現在最も人気のあるライブラリです。タグ付きテンプレートリテラルをうまく使った独自性と、明快なAPIでCSS in JSの火付け役にもなり、同じ思想を持った亜種ライブラリ(paypal/glamorous, zeit/styled-jsx等)が続々と出て来るなどある種のブームを巻き起こしました。
そもそも、Reactの登場でフロントエンド開発が「jQueryによるhtmlへの振る舞いの後付け」というスクレイピング的手法から、「構造と振る舞い(状態)を持ったUIの構築」というUIエンジニアリング的手法へとシフトする中、CSSだけが依然として「classNameへのマッピングによるスタイルの後付け」というスクレイピング的手法のままになっていました。styled-componentsはそこに焦点を当て、Reactでのフロントエンド開発を「構造と振る舞い(状態)、スタイルを持ったUIの構築」へと昇華させることに成功しました。
ここでは詳しいAPIについては割愛しますので、公式ページを参照ください。
styled-componentsで変わること
styled-componentsを使うことで、Reactのinline stylingやsassでのstylingから2つのことが大きく変わります。
コンポーネントとstyleのマッピングが無くなる
styled-componentsで定義するスタイル(= styled-component)はReactのコンポーネントそのものです。この仕組みにより、スクレイピング的なスタイリングではなく、レゴブロックを組み立てるようにUIを構築していくことができます。
// styled-components以前
<Button className='button'></Button>
<Button className='button button--primary'></Button>
// styled-components
<Button><Button/>
<Button primary></Button>
styled-componntsではよりシンプルに、セマンティックにすることができます。
ローカルスコープができる
CSS in JS全般に言えることですが、スコープができることにより、長い命名に悩まされる必要がなくなります。
具体的には、ファイル単位、コンポーネント単位のスコープとなるので、下記のようなBookコンポーネントとBookListコンポーネントがあった時に、どちらのコンポーネントでもWrapperという名前のコンポーネントを定義して使うことができます。
// Book.js
export default ({ book }) => (
<Wrapper>
<Title>{book.title}</Title>
<Author>{book.author}</Author>
</Wrapper>
)
const Wrapper = styled.div`
/* Book固有のWrapperスタイル */
`
const Title = styled.h2``
const Author = styled.p``
// BookList.js
import Book from './Book'
export default ({bookList}) => (
<Wrapper>
{bookList.map(book => <Book book={book}/>)}
</Wrapper>
)
const Wrapper = styled.div`
/* BookList固有のWrapperスタイル */
`
styled-componentsで変わらないこと
これまでと変わらないこともあります。枚挙にいとまがないので、簡単に列挙します。
- cssそのもの記述
- タグ付きテンプレートリテラルの中に書くcssは従来のcssそのものです。
- 定数管理、mixinなどの設計
- sass等で実践されていた、スタイルに関する定数やよく使うスタイルをmixinとして切り出すといった考え方は依然として必要になります。
- 見た目とレイアウトを分ける、などの基本的な設計
- 言語がcssからjsに移るものの、最終的にはピュアなcssとして吐き出されるので、css的に必要な設計は変わりません。
styled-componentsで最低限考えるべき設計
それでは、上記の変わること/変わらないことを踏まえた上で、どのような設計が必要になるでしょうか。ここでは最低限押さえておきたいポイントを紹介します。
フォルダ構成
どうコンポーネントを組み立てるかという話です。
Styled-componentsの作者の提案する構成は、ページやコンポーネント固有のものと複数のコンポーネントから使用される共通コンポーネントに大きくディレクトリを分けるアプローチです。
src
├── App
│ ├── Header
│ │ ├── Logo.js
│ │ ├── Title.js
│ │ ├── Subtitle.js
│ │ └── index.js
│ └── Footer
│ ├── List.js
│ ├── ListItem.js
│ ├── Wrapper.js
│ └── index.js
├── shared
│ ├── Button.js
│ ├── Card.js
│ ├── InfiniteList.js
│ ├── EmojiPicker
│ └── Icons
└── index.js
css - Styled-components organization - Stack Overflow
このアプローチはシンプルさが強みで、自分も小規模〜中規模なアプリケーションではこの構造に習っています。
一方で、アプリケーションの規模が大きくなるともう少し構造化したくなります。その場合、下記のようにAtomicデザイン的な構成を取るのがおすすめです。(コンポーネント名等は適当に書きました)
src
├── atoms
│ ├── button
│ │ ├── button.js
│ │ ├── index.js
│ │ └── extended-button.js
│ ├── heading
│ │ ├── h1.js
│ │ ├── h2.js
│ │ ├── h3.js
│ │ ├── index.js
│ ├── icons
│ │ ├── icons.js
│ │ ├── index.js
│ ├── input
│ └── ├── checkbox.js
│ ├── index.js
│ ├── text-input.js
│ └── textarea.js
├── molecules
│ ├── calendar
│ │ ├── calendar.js
│ │ ├── index.js
│ │ └── week.js
│ ├── modal
│ └── ├── index.js
│ └── modal.js
├── organisms
│ ├── footer
│ └── ├── footer.js
│ └── index.js
├── pages
│ ├── users
│ │ ├── users.js
│ │ ├── friend.js
│ │ └── index.js
│ ├── books
│ │ ├── books.js
│ │ ├── index.js
│ │ └── book.js
│ └── login
│ ├── login.js
│ └── index.js
└── templates
├── default-template
│ └── index.js
└── specific-template
└── index.js
styled-componentsは小さなUIからレゴブロックの用に構築していくという設計哲学上、atomicデザインと親和性が高いです。また、自分は各コンポーネントのディレクトリにindex.storie.jsを置き、storybookでスタイルガイドが見れるようにすることが多いです。
ファイル構成
何をどこに書くのかという話です。
Jsxとstyled-componentsの記述を下記のように分けて書くスタイルもよく見けます。Css modulesを使ったことのある人はこちらに慣れているのではと思います。
Card
├── index.js
└── style.js
個人的には、これらは分けずに同一ファイルに書く方がstyled-componentsの思想とマッチして好みです。ただし、ファイルを見た時にjsxの構造がわかるよう、jsx -> styled-componentsという順番で書いています。
// Componentが先
const Card = props => (
<Wrapper {...props}>
<Something>{props.children}</Something>
</Wrapper>
)
// styled-compoenentsは後
const Wrapper = styled.div``
const Something = styled.span``
命名ルール
ファイル単位、コンポーネント単位のスコープができ、冗長な命名は不要になりましたが、各コンポーネント内の要素の命名はリーダビリティの観点から一貫性のあるものにすべきです。自由な命名ができるからこそ、セマンティックな命名を心がけます。
- コンポーネントのラッパー:Wrapper, Box, Containerなど
- 個-集合の関係にあるコンポーネント:Item-ItemList、Item-Items、Item-ItemGroupなど
- レイアウト用途のもの:Flex, Fixed等
また、コンポーネントに渡すpropsの命名にも気をつけたいです。
- 型が推測し易い命名にする:isActive, hasHeaderなど(booleanを渡すと推測できる)
- htmlのアトリビュートとしてあるものはそのまま使う:disabled, checkedなど
定数を管理する
色やサイズは定数で管理しましょう。styled-componentsはjsなので、React側と定数を共通で管理できるのが良いところです。
オブジェクトとして定義し、タグ付きテンプレートの中で${}
で囲んで使うようにします。
色であれば、下記のようになります。
// const/Color.js
export default {
PRIMARY: '#XXXXXX',
SECONDARY: '#XXXXXX',
DANGER: '#XXXXXX'
}
// styled-components
const ErrorMessage = styled`
color: ${Color.DANGER};
`
サイズであれば、計算等し易いようにpxを除いて数値で管理します。
// const/Size.js
export default {
FONT: { SMALL: 10, BASE: 12, LARGE: 16 },
HEADER_HEIGHT: 40
}
// styld-components
const H2 = styled.h2`
font-size: ${Size.FONT.LARGE}px;
`
拡張性を高める
Styled-componentsでのコンポーネントの再利用は幾つかの方法がありますが、次のような優先順位で考えると良いでしょう。
- propsでstyleを出し分ける
- styled()またはextendで拡張する
- 別のコンポーネントにする
例えば、ボタンであればpropsを使って<Button primary /> <Button secondary /> <Button danger />
のように使い分けることができます。このような場合に、<ButtonPrimary /> <ButtonSecondary /> <ButtonDanger />
のように分けることはあまりすべきではありません。
また、やや話は逸れますがstyled-componentsで受け取るpropsはインターフェースとしてわかりやすくすべきです。例えば、大きさの違うモーダルがある時、下記のようにstringを渡す受け口を作るのもTypeScriptを使う場合に良い方法です。
// より明示的。TypeScriptの恩恵も受けられる
<Modal appearance='large' />
<Modal appearance='medium' />
// TSではUnion Typeを使う
type Props = {
appearance: 'medium' | 'large'
}
// 自由すぎる、他のpropsとの競合もありえる
<Modal large />
<Modal medium />
Propsで分けるには見た目に違いが大きい場合や、用途が例外的な場合は拡張をしましょう。
// 通常のButton
const Button = styled.button`
padding: 12px;
border-radius: 4px;
`
// コンバージョンだけに使うButton。
const ConversionButton = styled(Button)`
padding: 24px;
font-weight: 600;
`
拡張をするメリットは、primitiveである元のコンポーネントのコードを汚さないことです。ただし、拡張する側は元のコンポーネントに影響されるので、元のコンポーネントに変更を加える場合は注意が必要です。あまりに見た目や用途が異なる場合は別のコンポーネントとして作ることも考えましょう。
命名するほどでもないstyleを当てたい時
最近はGrid LayoutやFlexboxで無駄なdivを作らずに済むことも多いですが、実際にはButtonWrapperやFormWrapperのようなレイアウトのためだけのコンポーネントがどうしても必要な場合があります。「marginを少しつけたい」くらいでわざわざコンポーネント名を考えたくはないこともありますよね。
そんなときはv4から利用可能なcss propを使うのがおすすめです。
<div css="margin-top: 8px;">
<Button>余白がほしいボタン</Button>
</div>
ただし、複雑なスタイルを当てる場合は逆にjsxが読みにくくなってしまうので注意が必要です。
<div
css={css`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: ${Color.WHITE};
`}
>
<Item/>
<Item/>
</div>
このような場合は、styled-components化したほうが良いでしょう。
interpolationをクリーンに書く
StyledComponentにpropsを渡すことで、動的なスタイルを作ることができます(interpolation記法)が、多いと見づらくなってしまうことがあります。
そういうときは、下記のような記法を使うとクリーンになります
const styledButton = styled.div(props => `
background-color: ${props.color}
...
`)
スタイルとレイアウトを分けて考える(続編)
最も重要でstyled-componentsのコミュニティ内でも議論が多いポイントなのですが、これだけで1記事書けてしまうので別の機会に書きたいと思います。
まとめ
タイトルをcss設計としておきながら、実際にはコンポーネントそのものの設計に近い話になりました。実際、CSS in JSに対する否定的な意見として、「デザイナーが書けない」というものがあるのですが、Reactの世界においてスタイルとコンポーネントの設計はもはや分けられるものではありません。
コンポーネント時代のstylingを変えたstyled-components,おすすめです。
更新履歴
20180604: コンポーネントのstyle拡張のために使えるextend``APIはv4で非推奨になる予定のため、例から外しstyled()での拡張に置き換えました。
see https://github.com/styled-components/styled-components/issues/1546
20190831: css propを使った、わざわざ命名するほどでもないスタイルの当て方について記載しました。
20191114: interpolationをクリーンに書く方法について追記しました。