🏝️

Styled Systemを用いた快適UIスタイリング

2022/02/23に公開

styled-componentsを筆頭としたCSS-in-JSの登場以降、すっかりcss modulesやscssといったJSとCSSでファイルを分けた構成には戻れなくなってしまった僕ですが、最近では素のstyled-componentsでさえも煩わしさを感じています。

例えば以下のような記述を見てみましょう。

<MenuBox>
  <MenuTitle>hello</MenuTitle>
</MenuBox>

MenuBoxなるコンポーネントがどこかで定義されているんだなと思い、まずは定義元を探しに行きます。そして、恐らく以下の様に定義されているのを見つけるでしょう。

const MenuBox = styled.div`
  dispaly: flex;
  flex-direction: column;
  justify-content: center;
  width: 600px;
  @media screen and (min-width: 640px) {
    width: 320px;
  }
`

そしてこれを確認し終えた後はまた次に出現するMenuTitleコンポーネントの定義元を探しに行くという手順を踏むはずです。

このように、styled-componentsでコンポーネントのスタイル定義が別でなされている場合には、逐一定義元に戻って確認作業の繰り返しを強いられることになります。また、その間は周辺コンポーネントの構造も記憶しておかなければなりません。

CSSファイルが別で存在するよりは幾分マシですが、個人的にはこれでもまだしんどい。

加えて、レスポンシブ対応のメディアクエリの拡張性も乏しい。
このコンポーネントを別の場面で再利用する必要があり、そこでは別のサイズに変更する必要がある場合は以下のようにpropsを受け取り、動的にwidthを設定できるように作り替えなければなりません。

const MenuBox = styled.div<{width: string, smallWidth: string}>`
  dispaly: flex;
  flex-direction: column;
  justify-content: center;
  width: ${props => props.width};
  @media screen and (min-width: 640px) {
    width: ${props => props.smallWidth};
  }`

これでは読みやすくもない上に拡張性もそこまで高くないですね。
では、どうすればもう少し読みやすく拡張性が高いUIコンポーネントを定義できるでしょうか。

Utility Propsという考え方

上記問題を解決したUIコンポーネントライブラリの1つにChakra UIがあります。

Chakra UIではスタイリングをUtility Propsとして直接渡すことができます。例えば先ほど例に挙げたMenuBoxは以下の様に書くことができます。

<Box
  display="flex"
  flexDir="column"
  justifyContent="center"
  w={["340px", "600px"]}
/>

これであればいちいちコンポーネントの定義元に移動することなくスタイリングを確認することができるので快適に開発を進めることができます。Tailwind CSSにおけるUtility-Firstな思想と同じ様なものですね。

(余談ですが、人によってはこの書き方だとロジックとスタイリングが分離しにくいと感じるかと思います。僕の場合はReactのコンポーネントは全てPresentational/Containerパターンを採用し、ロジックや状態は全てContainer層に記述することでロジックとスタイリングを分離しています。なので、CSS-in-JSというよりはCSS-in-HTMLに近い感覚です。)

また、レスポンシブ対応についてもシンプルにw={["340px", "600px"]}といったようにwidthプロパティに配列形式で渡すだけで対応することができます。各ブレークポイントについては以下の様に定義します。

const breakpoints = createBreakpoints({
  sm: '30em',
  md: '48em',
  lg: '62em',
  xl: '80em',
  '2xl': '96em',
})

参考:Responsive Styles

Utility Propsを渡してコンポーネントを開発する利点は、単に読みやすく拡張性が高い以外にも以下の様な利点があります。

命名コストの低下

上記の様にインラインでスタイルを記述していけば、素のCSSのようにクラス名を考えたり、styled-componentsであれば再利用性の低い局所的な目的のコンポーネントの命名に悩まされる必要もなくなります。

ViewやBoxやButtonなどコアとなるコンポーネントだけ定義しておけばあとはそのコンポーネントに適当なpropsを渡して使い回すだけで済むので、可読性や開発速度向上に貢献します。

デザインシステムに則ることができる

通常のdivにインラインでCSSを記述する方法とこの方法の相違点は、この方法にはある程度制約が設けられている点です。(公式ドキュメントでもStyled System lets you quickly build custom UI components with constraint-based style props based on scales defined in your themeと謳われています)

これはどういうことかというと、themevariantを適切に設定しておけばデザインシステムに則ってスタイリングすることが可能で、コンポーネントにマジックナンバーやマジックカラーをインラインで埋め込まずに済むということです。また、誤って入れてしまったとしても一目瞭然なのでレビューで弾かれます。

各種UIコンポーネントライブラリへの対応

世はまさにUIコンポーネントライブラリ戦国時代です。

Chakra UIの他にもMUIやNative Base等様々なUIコンポーネントライブラリが世の中に溢れています。

そして、その全てのUIコンポーネントライブラリが上記記法に対応しているわけではありません。

どのUIコンポーネントライブラリにもそれぞれ素晴らしい点がありますが、各ライブラリによって書き方が違っていて、もし同じプロジェクト内に複数のコンポーネントライブラリを導入している場合は依存ライブラリによってスタイリング記法が異なり、統一感がなくなってしまいます。

例えばMUIでprops経由でスタイリングする場合、sxというプロパティ内に以下の様に記述するのが一般的です。

<Box
  sx={{
    boxShadow: 1, 
  }}
>

一方、普段僕が愛用しているNative BaseというUIコンポーネントライブラリでは、Chakra UIと同じ様にCSSの各プロパティ名を直接propsとしてコンポーネントに渡します。

<Box
  boxShadow={1}
>

どちらが書きやすいかというのは人それぞれだとは思いますが、複数人で開発する以上は書き方に統一感を持たせたいですよね。

そこでChakra UIやNative Baseのようにstyle propを全てのUIコンポーネントに対応させる際に役立つのがStyled Systemというライブラリです。

Styled Systemで既存コンポーネントを拡張する

まずは以下コマンドでStyled Systemを導入します。

npm i styled-system styled-components

styled-systemにはspacecolorといった様々なCSSプロパティ関数が用意されています。
以下の様に記述することで、ここで定義したBoxコンポーネントはcolor関数が持つcolorとbg(backgroundColorでも可)という2つのpropを使用することができる様になります。

import styled from 'styled-components'
import { color } from 'styled-system'

const Box = styled.div`
  ${color}
`

// 以下の様に使う
<Box color="#fff" bg="tomato">
  ボックスだよ
</Box>

ここではbackground-colorというCSSプロパティ名はbgという風に略されていますが、他のプロパティも同様に略した記法を用いることができます。ここで一例を挙げておきます。

Margin Props

m margin
mt margin-top
mr margin-right
mb margin-bottom
ml margin-left
mx margin-left and margin-right
my margin-top and margin-bottom

Padding Props

p padding
pt padding-top
pr padding-right
pb padding-bottom
pl padding-left
px padding-left and padding-right
py padding-top and padding-bottom

参考: https://styled-system.com/getting-started

ところで、styled-componentsはテンプレートリテラル以下にCSSを記述しスタイリングします。これはタグ付きテンプレートリテラルと呼ばれるもので、バッククオート以下を引数に受け取ったstyled()関数の様に動きます。

つまり、上記Box関数は以下の様に書き直すことができます。

import { color } from 'styled-system'

const Box = styled.div(color);

また、styled-systemから提供される型を使用することで型推論に対応したコンポーネントを作成できます。

import { color, ColorProps } from 'styled-system'

const Box = styled.div<ColorProps>(color);

これを応用すると、MUIのようなUIコンポーネントライブラリにも以下の様にラップすることで同じ様な記述を行うことができます。

src/components/common/Button.ts
import React, { FC, memo, HTMLAttributes } from "react";
import { ButtonStyleProps, buttonStyle } from "styled-system";
import { Button as _Button } from '@material-ui/core'
import styled from "styled-components";

type Props = ButtonStyleProps & HTMLAttributes<HTMLButtonElement>;

export const Button: FC<Props> = memo(({ children, ...props }) => {
  return <ButtonRoot {...props}>{children}</ButtonRoot>;
});

const ButtonRoot = styled(_Button)<ButtonStyleProps>(buttonStyle);

加えて、以下の様にvariantを設定することでFigma等のプロトタイピングツール上のデザインシステムで定義されたvariantとも揃えることができます。

import { ButtonStyleProps, buttonStyle, variant } from "styled-system";
const ButtonRoot = styled(_Button)<ButtonStyleProps>(
  buttonStyle,
  variant({
    variants: {
      primary: {
        color: "white",
        bg: "primary",
      },
      secondary: {
        color: "white",
        bg: "secondary",
      },
    },
  })
);

// 以下の様に使う
<Button variant="primary">ボタンだよ</Button>

hover時のスタイリングなども予めvariantで定義しておくのがよいでしょう。

僕の場合は既存UIコンポーネントへのオーバーライドであれ、通常のdivの拡張であれ、上記のように定義した共通コンポーネントは全てsrc/components/common配下におき、一括エクスポートして運用しています。

src/components/common/index.ts
export * from './Button/Button'
export * from './HStack/HStack'
...
export * from '/View/View'

まとめ

styled-systemを用いればデザインシステムに則りながらインラインでCSSをコンポーネントに記述することができるのでコンポーネントの構造も把握しやすくなり、かつ開発速度向上にも貢献します。

プロパティ名を過度に略しすぎる部分(margin-top→mt, background→bg etc)やロジックとスタイリングの混在が気になるなど人を選びそうな部分はありそうな気がしつつも、慣れてしまえばもう以前のような生活には戻れないですね。

Styled Systemはいいぞ

Discussion