styled-system でコンポーネントスタイルの一貫性を保つ

この投稿は Goodpatch Advent Calendar 2022 12日目の記事です。

フロントエンドエンジニアの 上垣です。

最近は、仕事でもプライベートでも Chakra UI をよく利用します。Chakra UI が優れていると思う点は、Props でスタイルを直接指定できることと、テーマを管理、拡張するやり方がとても簡単でスマートなところです。この特徴により、開発のスピードが爆発的に上がったり、デザインの一貫性が保ちやすいと感じています。

Chakra UI などの特定のライブラリを使わずに自分でデザインシステムを作るなら、同じような props, Theme 拡張を実装したいなと思い、Chakra UI のやり方調べたところ、Chakra の Theming は styled-system というライブラリが定義している、Styled System Theme Specification に基づいて設計されていることがわかりました。

具体的なコードを読む前に、そもそも styled-system って何なんだろう? と疑問に思って調べてみたので、この記事では、styled-system の考え方をざっくり紹介できればと思います。

注: 記事をある程度書いてから気づいたのですが、styled-system は現時点で2年近く更新されていないので、ライブラリそのものを実際のプロジェクトに導入するのは難しいと思います。あくまで考え方、設計の参考にしていただければと思います。

styled-system とは?

styled-system は、コンポーネントに、テーマに基づいたスタイルプロパティを追加する関数を提供する、低レベルなライブラリです。

styled-componentsemotion などの、CSS in JS ライブラリと組み合わせて利用することで、UI コンポーネントを素早く構築することができます。

この記事では、React と styled-components と組み合わせて利用する例を紹介します。

コンポーネントに props を追加する

例として、色に関するスタイル(color, background-color, opacity)を、コンポーネントの props に追加する方法をみてみます。

import styled from "styled-components";
import { color, ColorProps } from "styled-system";

const Box = styled.div<ColorProps>`
  ${color}
`

export default Box

こうすると、Box コンポーネントの props に color, bg, backgroundColor, opacity が追加され、props の値が、コンポーネントのスタイルに適用されます。

<Box color="red" opacity="0.5">
   Read Text
</Box>

// { color: red; opacity: 0.5;}

color のほかにも、例えば margin と padding を props に追加したいなら、 space 関数を、width, height なら layout を import すれば OK です。(styled-system で利用できる関数と、追加される props は、こちら で確認できます。)

また、プロパティ名に関して、backgroundColor が bg で指定できるように、styled-system では独自の短縮エイリアスが定義されています。css プロパティと props の対応は、こちらで確認できます。

CSS in JS に関数を導入するだけで、コンポーネントのスタイリングに一貫性と柔軟性を持たせられるのはとても大きなメリットだと思います。

テーマを定義する

上記の例で color 関連の Props をコンポーネントに追加しましたが、このままだと CSS で有効なカラーを全て指定できてしまいます。(e.g. ”red”, “#ff0000”)

これに制約を与えて、より一貫性のあるUI を作成するために、テーマを利用できます。

テーマの仕様は styled-system によって定められているので、この仕様に沿ってテーマを定義していきます。styled-system はデフォルトのテーマを持っているので、必要なテーマだけ上書きしていきます。テーマが定義できたら、styled-component の場合は、ThemeProvider に渡せば OK です。

colors と space を上書きする例

import { ThemeProvider } from "styled-components";
import type { AppProps } from "next/app";

const theme = {
  colors: {
    black: "#000e1a",
    white: "#ffffff",
    blue: "#007ce0",
    navy: "#004175",
  },
  space: [0, 4, 8, 16, 32],
};

export default function App({ Component, pageProps }: AppProps) {
  return (
    <ThemeProvider theme={theme}>
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

これによって、props の値が テーマオブジェクトの key として存在していればオブジェクトの値を、存在しなければ props の値をそのまま適用するようになります。

<Box color="navy"></Box> // => color: #004175;
<Box color="red"></Box> // => color: red;

なお、配列で定義されたテーマは、props からは index で値を参照できます。

<Box m="4"></Box> // => margin: 32px

また、styled-components 内で theme を参照する場合は、styled-system の theme-get が利用できます。

import { themeGet } from "@styled-system/theme-get";

const Box = styled.div<Props>`
  ${color}
  padding: ${themeGet("space.4", "16")}px; // padding: 32px;
`;

export default Box;

props の追加とテーマの設定についての基本的な紹介は以上です。

ほかにも、レスポンシブスタイルや、variant、default 値の定義もできるので、気になった方は document を参照してみてください。

軽く触ってみて、とてもシンプルな API で、柔軟な props 追加やテーマの適用が実現できることがわかりました。 今後独自のデザインシステムを構築する際は、styled-system ベースで構築されているライブラリを参考にしつつ、styled-system を基にしたテーマ設計を検討してみたいと思います。