こちらは GAOGAO Advent Calendar 2020 17日目の記事になります.
こんにちは, GAOGAO の案件に携わらさせて頂いている こうりん と申します.
普段はフロント寄りのWebエンジニアとして主に働いており, React (+ Next.js) を使ったコードを書いています.
この記事では, Reactのスタイル用ライブラリである styled-components 向けのユーティリティライブラリ styled-system の紹介をしていきます.
1. styled-components の概要
まず, styled-components について簡単に説明します.
styled-components は CSS in JS ライブラリの一つです。
Reactのコンポーネントにスタイルを適用するには, 大きく分けると3つあります.
- props の style にオブジェクトを渡す
- CSS や SASS を JSX/TSXファイル とは別に書き, コンポーネントには className または styleName を指定する
- CSS in JS ライブラリを使う
1が最も単純な方法ですが, 擬似要素セレクタ (:before, :hover) やメディアクエリが指定できないというデメリットがあります.
2は従来通りの方法です. コンポーネントとスタイルを明確に分ける事ができ, またSASS, SCSSを使う事ができるので, チーム構成によってはこちらのやり方が適している場合もあるかと思います.
今まではユニークなクラス名定義に注意を払う必要がありましたが, Webpackのプラグインを使うことで, 自動でローカルスコープに変換することもできます (CSS Modules)
3はJSX ファイル内でスタイルを定義する方法です. 1と違う点はライブラリ独自のやり方でスタイルを定義します. 有名なライブラリではタグ付きテンプレートリテラルを使うことで、CSSそのままの書き方で書く事ができます.
import styled from 'styled-components';
const Text = styled.p`
margin-top: 16px;
margin-bottom: 16px;
color: ${( props ) => props.color};
`;
Text.defaultProps = {
color: '#000000'
};
...
<Text color="#ff0000">Hello!</Text>
上のコードは今回使う styled-components での書き方の一例です.
pタグにスタイルを適用させた新たなコンポーネント Text を定義しています. ` `で囲まれている内部では, CSSを記述することができます. 内部では関数を埋め込む事ができ, propsに渡した値で動的にスタイルを決定することができます.
CSS in JS ライブラリを用いることで, CSSの表現力を落とす事なく, またpropsを用いて自由度を持たせたスタイルを容易に定義することができます.
2. styled-system の概要
本題の styled-system の紹介です. styled-system は styled-components 向けのユーティリティライブラリです. styled-components では関数を埋め込むことで, props 経由でスタイルを変えることができますが, 一々関数を定義して埋め込む必要があります. styled-system では, よく使うプロパティ用の関数を提供します.
import styled from 'styled-components';
import { color, typography } from 'styled-system';
const Text = styled.p`
${color}
${typography}
`;
<Text color="#ff0000" fontSize="24px">Hello!</Text>
Text には styled-system が提供している color と typography を埋め込みました. こちらは名前の通り, 色と文字に関するスタイルのための関数です. この2つを埋め込んだ事により, props経由で color, font-size などが変更できるようになります. このように, styled-system を用いることで, コンポーネント毎に関数を定義して埋め込む必要が無くなります.
import styled from 'styled-components';
import { color, typography } from 'styled-system';
const Text = styled('p')(compose(color, typography));
<Text color="#ff0000" fontSize="24px">Hello!</Text>;
また, 独自にスタイルを定義せず, styled-system の関数だけを埋め込みたい場合は, 上のような書き方も可能です. (公式ドキュメントによると, composeを使うことでパフォーマンス改善の効果があるようです)
3. styled-system の便利な機能
基本的な使い方については, 前の章で説明したので, ここからは styled-system の便利な機能について説明します.
Theme
context を利用して, 共通のスタイルを定義することができます.
まず, 共通なスタイルをまとめたオブジェクト(theme) を定義し, それを ThemeProvider に渡します.
(ThemeProvider 以下のコンポーネントで使えるようになるので, ThemeProviderは UI層より上のコンポーネントに置きます)
export const theme = {
space: ['0px', '4px', '8px', '16px', '24px', '32px'],
colors: {
white: '#ffffff',
black: '#000000',
red: '#ff0000'
},
fontSizes: ['12px', '14px', '16px', '18px', '20px', '23px'],
fonts: {
primary: `'Inter', sans-serif`
},
};
import { ThemeProvider } from 'styled-components';
import { theme } from './theme';
const App = () => {
return (
<ThemeProvider theme={theme}>
<Page />
</Theme>
);
}
import styled from 'styled-components';
import { color, typography, space } from 'styled-system';
const Text = styled('p')(compose(color, typography, space));
Text.defaultProps = {
fontFamily: 'primary',
};
...
<Text color="red" fontSize={2} mt={3}>Hello!</Text>;
上のコード例では, まず, spaces, colors, font-sizes, fonts などをthemeオブジェクト内に定義し、それをルートコンポーネント内で ThemeProvider に渡しています.
そして, 各コンポーネントでは, 値の代わりにthemeで定義されたやキーを与えることで, その値を参照することができます.
上の Text では color="red", fontSize={2}, mt={3}
と props に渡していますが、実際には theme の値を参照しており, 描画時には color: #ff0000; font-size: 16px; margin-top: 16px;
となります.
Variant
複数のスタイルを1つのpropsで切り変えることができます.
例えば Button では, 背景色によって文字色も変化するかと思います.
このような場合に, variantを使うことでスタイルのグループを定義して, キーで複数のスタイルを同時に切り替えることができます.
import styled from 'styled-components';
import { variant } from 'styled-system';
const variants = {
primary: {
color: '#ffffff',
backgroundColor: '#1D3461',
border: 'none',
},
disabled: {
color: '#ffffff',
backgroundColor: '#a9a9a9',
border: 'none',
},
success: {
color: '#ffffff',
backgroundColor: '#5AB203',
border: 'none',
},
warning: {
color: '#ffffff',
backgroundColor: '#F98120',
border: 'none',
}
};
export const Button = styled.button<ButtonProps>`
${variant({ variants })}
border-radius: 19px;
font-size: 14px;
height: 38px;
line-height: 22px;
letter-spacing: 0;
cursor: pointer;
text-align: center;
&:focus {
outline: none;
}
`;
Button.defaultProps = {
variant: 'primary',
fontFamily: 'primary',
};
<Button variant="success">Next</Button>
上のコード例では、まず variants オブジェクトを定義しています. variants オブジェクト内では複数のスタイルを1つのグループとし, primary, success などのキーで配置しています. Button コンポーネントを定義する際に, variant関数に variants を渡して埋め込みます.
Buttonコンポーネントを使う際には variant にキーを与えることで, スタイルを切り替える事ができます. 例では variant="success"
を渡しているので, color: #ffffff; background-color: #5AB203; border: none;
が適用されます. これを variant="warning"
などに変えると, ボタンの背景色や文字色が変わります.
レスポンシブ対応
画面幅によって適用させるスタイルを切り替えることもできます.
まず, theme に breakpoints を入れます.
そして, props経由でスタイルを指定する際に, 値の代わりに breakpoints をキーとしたオブジェクトを渡します. (_ はデフォルトの場合)
const breakpoints = ['0px', '600px', '960px', '1280px', '1920px'];
breakpoints.xs = breakpoints[0];
breakpoints.sm = breakpoints[1];
breakpoints.md = breakpoints[2];
breakpoints.lg = breakpoints[3];
breakpoints.xl = breakpoints[4];
export default breakpoints;
import breakpoints from './breakpoints.js';
export const theme = {
...,
breakpoints
}
<Flex flexDirection={{ _: 'row', md: 'column' }}>
<Card />
<Card />
...
</Flex>
Flexコンポーネント (flexbox関数 を埋め込んだコンポーネント) の flexDirection にオブジェクトを渡しています.
960pxを境に Card の整列方向が切り替わります.
4. まとめ
業務で関わっているプロジェクトでは, フロントエンドはJS/TSを主に書くエンジニア2~3人で開発することが多いので、簡単にスタイルを定義できてコンポーネントとスタイルを一体化して扱える styled-components (及び styled-system) はメリットが大きいです.
styled-system を使うと, props経由で簡単に色々なスタイルを定義できるのが便利ですが, propsが肥大化しがちなので variant や defaultProps を駆使して props に渡す値の数を抑える工夫が必要になってきます.
また, styled-components + styled-system に限った話ではありませんが, テキストやボタンのUIパーツのスタイルとマージンなどのスペース・配置に関するスタイルはコンポーネントを分けて定義した方が, 可読性と修正の容易さを高く保てると思います.