React + Ant Design なプロジェクトにおけるスタイリング手法について
この記事は React Advent Calendar 2022 の25日目の記事です。
はじめに
みなさん、Ant Design は使ってますか? Ant Designのスタイルの上書きはどうしてますか?
Ant Design は React 向けの UI コンポーネントライブラリです。私が担当しているプロジェクトでは、ほとんどの UI を Ant Design で作ってますが、スタイリング手法に問題を抱えています。
スタイリング手法は、Reactにまつわる話題の中でもホットなトピックの1つだと思いますが、「Ant Designとの共存」に特化した記事は見つかりませんでした。それなら自分で書こう!と思い、この記事を書いてます。
現状のスタイリング手法を改善しつつ、Ant Design とも上手く共存できる方法をいくつか検討したところ、CSS Modules または、代表的な CSS-in-JS ライブラリの styled-components、Emotion であれば目的を達成できると思いました。
本記事での前半では、そのような結論に至った理由を紹介します。後半では、デモアプリのソースコードを見ながら、CSS Modules、styled-components、Emotion について、基本的な使い方と Ant Design のスタイルの上書き方法を紹介します。
現状の問題点
プロジェクトでは以下のようにスタイルを管理してます。
-
src/sass/index.scss
で全ての SCSS ファイルを@import
-
src/index.jsx
でsrc/sass/index.scss
をimport
この手法の問題点は、個々の SCSS ファイルに記述したスタイルがグローバルに影響することです。例えば、User.jsx
だけで適用するために User.scss
で指定した .container
のスタイルが、Post.jsx
などにも適用される可能性があり、管理を難しくしています。この問題を解決する方法を模索しました。
解決方法の模索
検討した選択肢
React のスタイリング手法に関する記事を読み漁り、以下の方法を検討しました。
- BEM + SCSS
- インラインスタイル
- Tailwind CSS
- CSS Modules
- styled-components (CSS-in-JS)
- Emotion (CSS-in-JS)
参考
- Reactのコンポーネントのスタイリングをどうやるか
- SassからCSS Modules、そしてstyled-componentsに乗り換えた話
- Reactにおけるスタイリング手法まとめ
- ReactのCSSの選択肢を比較してみた
選定条件
今回は以下の条件で解決方法を模索しました。
- SCSS からの置き換えコストが低い
- 一つの方法に統一できる
- Ant Design の CSS クラス(
.ant-modal
など)のスタイルを上書きしやすい
検証の結果、上記の6つの選択肢の中で条件に合致するのは、CSS Modules、styled-components、または Emotion だけなのでは、という結論に至りました。次のセクションで、他の選択肢が条件に当てはまらない理由を解説します。
不採用にした選択肢について
BEM + SCSS
BEM とは CSS クラスや SCSS ファイルの命名などに規則を設けることで、スコープを設ける手法です。例えば、クラス名は block__element--modifier
の形にする必要があります。
( 参考: 一番詳しいCSS設計規則BEMのマニュアル )
Ant Design のコンポーネントには、既に決められた CSS クラスが設定されており、BEM のルールを適用させるのは困難なので、不採用としました。
インラインスタイル
インラインスタイルとは、style 属性 にオブジェクト形式で CSS のプロパティを設定する手法です。以下のように書くことができます。
const style = { backgroundColor: 'red' }
<div style={style}>Hello World</div>
不採用とした理由は以下の通りです。
- SCSS の記述をオブジェクト形式の記述に変換するのが辛い
- 公式で非推奨
- 場合によっては、Ant Design の スタイルの上書きが難しい
Tailwind CSS
Tailwind CSS は Bootstrap と並んで人気のある CSS フレームワークです。予め設定された CSS クラスを適用することで、スタイルを当てることができます。例えば以下のように書くことができます。
( 公式ドキュメント から引用 )
<h1 className="text-3xl font-bold underline">
Hello world!
</h1>
不採用とした理由は以下の通りです。
- 既存の SCSS の記述からの置き換えが大変そう
- 場合によっては、Ant Design の スタイルの上書きが難しい
Ant Design の スタイルの上書きについて
インラインスタイルと Tailwind CSS について、「場合によっては、Ant Design の スタイルの上書きが難しい」を不採用の理由の1つに挙げていますが、深掘りします。
本記事でも扱う Ant Design の Modal は、以下のような構造を持つ HTML をレンダリングします。
( class
以外の属性を削除するなど、加工してます。)
<div class="ant-modal-root css-1wismvm">
<div class="ant-modal-mask"></div>
<div class="ant-modal-wrap">
<div class="ant-modal css-1wismvm">
<div class="ant-modal-content">
<button class="ant-modal-close">
<div class="ant-modal-header">
<div class="ant-modal-title">Basic Modal</div>
</div>
<div class="ant-modal-body">
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</div>
<div class="ant-modal-footer">
<button class="ant-btn css-1wismvm ant-btn-default">
<span>Cancel</span>
</button>
<button class="ant-btn css-1wismvm ant-btn-primary">
<span>OK</span>
</button>
</div>
</div>
</div>
</div>
</div>
Ant Design の Modal では、bodyStyle
、className
、style
プロパティが提供されてます。
- bodyStyle
-
.ant-modal-body
に独自のスタイルを適用できる
-
- className
-
.ant-modal
に 独自のCSS クラスを追加できる
-
- style
-
.ant-modal
に独自のスタイルを適用できる
-
では .ant-modal-content
のスタイルを上書きしたい!と思った時に、インラインスタイルや Tailwind CSS ではどうすれば良いのでしょうか。 私は良い方法が思いつかないです。例えば、Ref などを使って 取得した Dom 要素に適用することはできそうですが、面倒ですよね...
一方で、CSS Modules、styled-components、または Emotion を使うと、このような場合でも比較的簡単にスタイルを上書きできます。
デモアプリについて
概要
Ant Design の Modal を用いて、「最大化機能」を持つモーダルを、以下の4つの方法で実装しました。公式では最大化機能は提供されてないので、Ant Designのスタイルを一部上書きしてます。
- CSS Modules
- styled-components
- @emotion/react
- @emotion/styled
実際の動き
4つのボタン、モーダルは全て同じ動きをします。
ソースコードはGitHubにあげてます。
ディレクトリ構成
src
├── components
│ ├── CssModules
│ │ ├── index.jsx
│ │ └── style.module.scss
│ ├── EmotionReact
│ │ ├── index.jsx
│ │ └── style.js
│ ├── EmotionStyled
│ │ ├── index.jsx
│ │ └── style.js
│ └── StyledComponents
│ ├── index.jsx
│ └── style.js
├── App.jsx
├── index.css
├── index.jsx
App.jsx
CSS Modules
概要
CSS Modules とは、CSS のクラス名にローカルスコープを適用するための仕組みです。大筋の仕様は css-modulesで定義されており、css-loader などで実装されてます。
Create React App で作成したプロジェクトでは、react-scripts が様々なライブラリを隠蔽・管理してくれてますが、css-loader もその1つです。更に react-scripts@2.0.0
以降では、設定不要で CSS Modules の機能を使えます。
本記事では、Create React App における CSS Modules の使い方を解説します。
基本的な使い方
今回は SCSS を使いたいので、sass
のインストールのみ必要です。
$ yarn add sass
あとは、Create React App の Adding a CSS Modules Stylesheet に従い、SCSS のファイル名を *.module.scss
のようにしつつ、以下のように適用するだけで、ローカルスコープを持つスタイルを当てることができます。
.container {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-top: 16px;
}
import styles from './style.module.scss';
<div className={styles.container}>
{/* 省略 */}
</div>
Chrome DevTools で確認すると、上記コードは以下のように反映されてます。
.style_container__mLJ9W {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-top: 16px;
}
<div class="style_container__mLJ9W">
<!-- 省略 -->
</div>
このように、一意のクラス名が自動生成されるので、ローカルスコープを実現できるわけです。
Ant Design のスタイルを上書き
結論から言うと、状態に応じてクラス名を切り替えた上で、:global クラス を使用し、以下のようにすることで、スタイルの上書きを実現できました🎉
classnames について
classnames は、条件に応じて className
を設定する時などに便利なライブラリです。Reactの公式ドキュメントでも紹介されてます。 本記事の執筆時点では、Create React App で作成したプロジェクトでは、デフォルトでインストールされてます。
今回の例では、styles.modal
は常に適用され、styles.fullscreen
は、isFullscreen
が true
の時のみ適用されます。このように CSS Modues では、クラス名を操作することで、アプリの状態をスタイルに反映させることができます。(後述の styled-components や Emotion では、状態に応じてスタイル自体を動的に書き換えます。)
:global クラスについて
ここが重要です。:global
を適用したセレクタは、CSS Modules の適用外となり、グローバルスコープになります。例えば、.ant-modal-title
の箇所は以下のように変換されます。
変換前
.modal {
:global(.ant-modal-title) {
display: flex;
justify-content: space-between;
align-items: center;
.icons {
display: flex;
span {
margin-left: 8px;
}
}
}
// 省略
}
変換後
.style_modal__om2fz {
.ant-modal-title { // .ant-modal-title はそのまま
display: flex;
justify-content: space-between;
align-items: center;
.style_icons__tq-Dt {
display: flex;
span {
margin-left: 8px;
}
}
}
// 省略
}
.style_modal__om2fz
でローカルスコープを実現しつつ、Ant Design のスタイルを上書きできました。
参考
styled-components
概要
styled-components は CSS-in-JS の代表的なライブラリです。以下の画像は、State of CSS 2022 の CSS-in-JS 部門 における使用率のランキングです。styled-components が堂々の一位となってますね👏
styled-components では、後述の Emotion と同様に、タグ付きテンプレートリテラルという JavaScript の記法を用いてコンポーネントにスタイルを適用させます。タグ付きテンプレートは以下のようなものです。
const helloTag = (strArr, name) => {
console.log(strArr); // [ 'Hello ', '-san!!' ]
return `${strArr[0]}${name}${strArr[1]}`;
}
const name = 'Yamada'
const output = helloTag`Hello ${name}-san!!`; // これがタグ付きテンプレートリテラルの記法
console.log(output); // Hello Yamada-san!!
基本的な使い方
インストール / import
$ yarn add styled-components
import styled from 'styled-components';
基本の形(新しくコンポーネントを作成)
( 参考: https://styled-components.com/docs/basics#getting-started )
構文
const コンポーネント名 = styled.<HTMLのタグ>`
/* CSS / SCSS でスタイルを記述 */
`;
具体例
import styled from 'styled-components';
export const StyledContainer = styled.div`
width: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-top: 16px;
`;
<StyledContainer>
{/* 省略 */}
</StyledContainer>
Chrome DevTools で確認すると、上記コードは以下のように反映されてます。スタイルタグを生成しつつ、一意の class が付与されているようです。
<style data-styled="active" data-styled-version="5.3.6">
.ePqYIq {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
margin-top: 16px;
}
/* 省略 */
</style>
<div class="sc-bcXHqe ePqYIq">
<!-- 省略 -->
</div>
styled-components で生成した、スタイルを適用させたコンポーネントのことを、Styled Component と呼びます。Styled Component の名前は何でもOKですが、頭に Styled
をつけることが多いです。タグつきテンプレートリテラルは書き方に癖がありますが、SCSS の記述をそのままコピペできますし、簡単に導入できそうです。次に、既存のコンポーネントにスタイルを適用させる方法を紹介します。
既存のコンポーネントに適用
( 参考: https://styled-components.com/docs/basics#extending-styles )
構文
const コンポーネント名 = styled(コンポーネント)`
/* CSS / SCSS でスタイルを記述 */
`;
具体例
import { Button } from 'antd';
import styled from 'styled-components';
const StyledButton = styled(Button)`
width: 320px;
height: 40px;
font-size: 16px;
`
<StyledButton type="primary">
styled-components
</StyledButton>
とっても直感的!次は、既存のコンポーネントに、動的にスタイルを適用させる方法を紹介します。
動的にスタイルを適用
( 参考: https://styled-components.com/docs/basics#adapting-based-on-props )
生成した Styled Component に、状態を保持する変数を props として渡すことで、動的にスタイルを適用させることができます。
構文
const コンポーネント名 = styled(コンポーネント)`
height: ${props => props.isFullscreen && '100vh'};
`;
具体例
import { Modal } from 'antd';
import styled from 'styled-components';
const [isFullscreen, setIsFullscreen] = useState(false);
const StyledModal = styled(Modal)`
top: ${props => props.isFullscreen && 0};
padding: ${props => props.isFullscreen && 0};
height: ${props => props.isFullscreen && '100vh'};
margin: ${props => props.isFullscreen && 0};
/* 省略 */
`;
<StyledModal
closable={false} // antd の Modal が持つ prop
isFullscreen={isFullscreen} // オリジナルの prop
// 省略
>
<p>styled-components</p>
{/* 省略 */}
</StyledModal>
最後に、ここまでの内容を踏まえて、Ant Design のスタイルを上書きした結果を紹介します。
Ant Design のスタイルを上書き
以下のようにすることで、スタイルの上書きを実現できました🎉
Emotion
概要
styled-components と同様に、CSS-in-JS の代表的なライブラリです。以下の画像は、npm trends で、主要な CSS-in-JS ライブラリのダウンロード数を比較したものです。
Emotion は State of CSS 2022 では4番手ですが、npm trends においては、2022/12/11 時点で、Emotion のパッケージである、@emotion/styled と @emotion/react が ワンツーフィニッシュをきめてます👏
Emotion では styled-components と同様にタグつきテンプレートリテラルを使用しますが、大きく分けて2つの方法で使用できます。1つが フレームワークに依存しない方法。もう一つが React で使う方法 です。さらに、React で使う方法 は、The css Prop と Styled Components の2通りあります。また、Emotion ではタグつきテンプレートリテラル意外にも、オブジェクトスタイル での記述をサポートしてます。本記事では、The css Prop と Styled Components の2つを、タグつきテンプレートリテラル記法で紹介します。
基本的な使い方【The css Prop 編】
The css Prop は、React で Emotion を使用する際の主要な方法です。styled-components では、JSX タグや既存のコンポーネントをラップするような形で、スタイルが適用された新たなコンポーネントを生成します。一方で、Emotion の css Prop では、JSX タグや既存のコンポーネントに、css Prop を渡す形でスタイルを当てます。
インストール / import
$ yarn add @emotion/react
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
JSX Pragma について
import 文の上部に /** @jsxImportSource @emotion/react */
を記述してますが、これがないとスタイルが反映されません。その理由を調べてみたので紹介します。
前提として、Create React App において、JSX は Babel などのコンパイラを通して、JavaScriptに変換されます。具体的には、React.createElement(...)
の形に変換されます。
サンプルコードを見る
// 変換前
import React from 'react';
function App() {
return <h1>Hello World</h1>;
}
// 変換後
import React from 'react';
function App() {
return React.createElement('h1', null, 'Hello world');
}
React 17 以降かつ Create React App 4.0 以降であれば、_jsx(...)
の形に変換されます。
サンプルコードを見る
// 変換前
import React from 'react';
function App() {
return <h1>Hello World</h1>;
}
// 変換後
// Inserted by a compiler (don't import it yourself!)
import {jsx as _jsx} from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}
css Prop を使うには、React.createElement()
や _jsx()
の代わりに、Emotion の jsx()
を使用する必要があります。その為の方法として、以下の2つがあります。
Create React App のように Babel の設定をカスタマイズできない場合は、Babel Preset ではなく、JSX Pragma を設定します。JSX Pragma を設定することで、JSX → JavaScript の変換方法として、Emotion の jsx()
を指定することができます。設定方法は以下の通りです。
- React 17 以降かつ Create React App 4.0 以降
- ファイルの上部に
/** @jsxImportSource @emotion/react */
を記述する
- ファイルの上部に
- それ以外
- ファイルの上部に
/** @jsx jsx */
を記述する
- ファイルの上部に
参考
- The css Prop (Emotion の公式ドキュメント)
- 新しい JSX トランスフォーム (React の公式ドキュメント)
- 新旧 JSX Transform と @jsx・@jsxImportSource がやっていることにちょっとだけ詳しくなる
基本の形(既存の JSX タグに css Prop を渡す)
構文
const 変数名 = css`
/* CSS / SCSS でスタイルを記述 */
`;
具体例
import { css } from '@emotion/react'
const containerStyle = css`
width: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-top: 16px;
`;
<div css={containerStyle}>
{/* 省略 */}
</div>
Chrome DevTools で確認すると、上記コードは以下のように反映されてます。styled-components と同様に、スタイルタグを生成しつつ、一意の class が付与されているようです。
<style data-emotion="css" data-s="">
.css-obyd3-EmotionReact {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
margin-top: 16px;
}
</style>
<div class="css-obyd3-EmotionReact">
<!-- 省略 -->
</div>
個人的には、styled-components のように <StyledContainer>
を作成するよりも、既存の JSX タグである <div>
に css Prop としてスタイルを渡す方が、コードがスッキリして好みです。
既存のコンポーネントに適用
構文
スタイルを定義した変数を css Prop に渡すだけなので、基本の形と同じです。
具体例
import { Button } from 'antd';
import { css } from '@emotion/react'
export const buttonStyle = css`
width: 320px;
height: 40px;
font-size: 16px;
`;
<Button type="primary" css={buttonStyle}>
EmotionReact
</Button>
sytled-components も直感的ですが、やはり prop を渡すだけというシンプルさはとても魅力的ですよね。次は、既存のコンポーネントに、動的にスタイルを適用させる方法を紹介します。
動的にスタイルを適用
styped-components では、生成した Styled Component に 状態を保持する変数を props として渡すことで、動的なスタイリングを実現しました。Emotion の The css Prop では、css prop を、状態を保持する変数を引数として受け取る関数として定義することで、動的なスタイリングを実現します。
具体例
import { Modal } from 'antd';
import { css } from '@emotion/react'
const [isFullscreen, setIsFullscreen] = useState(false);
// State を引数として受け取る関数を定義
const modalStyle = (isFullscreen) => css`
top: ${isFullscreen && 0};
padding: ${isFullscreen && 0};
height: ${isFullscreen && '100vh'};
margin: ${isFullscreen && 0};
/* 省略 */
`;
<Modal
closable={false} // antd の Modal が持つ prop
css={modalStyle(isFullscreen)} // css Prop
// 省略
>
<p>EmotionReact</p>
{/* 省略 */}
</Modal>
最後に、ここまでの内容を踏まえて、Ant Design のスタイルを上書きした結果を紹介します。
Ant Design のスタイルを上書き【The css Prop 編】
以下のようにすることで、スタイルの上書きを実現できました🎉
基本的な使い方【Styled Components 編】
インストール / import
$ yarn add @emotion/styled
import { styled } from '@emotion/styled'
使い方は styles-components と全く同じなので割愛します。公式ドキュメント でも以下のように記載されてます。
styled was heavily inspired by styled-components and glamorous.
(訳)この機能は、styled-components と glamorous に大きな影響を受けてます。
Ant Design のスタイルを上書き【Styled Components 編】
一応試しましたが、本当に全く同じでした!
余談
CSS-in-JS のパフォーマンスの話
今回検証した styled-components と Emotion は、CSS-in-JS の中でも「ランタイム CSS-in-JS」と呼ばれており、パフォーマンス悪化の指摘をされてます。ランタイム CSS-in-JS では、レンダリング時に JavaScript を実行することで、JSX とスタイルを関連付けたり、CSSスタイルタグを動的に生成したりします。そのため、「スタイリング」という目的に対して「JavaScriptの実行」というオーバーヘッドが生じます。
そんな中、次世代の CSS-in-JS ライブラリとして、「ゼロランタイム CSS-in-JS」を謳う linaria や vanilla-extract なども登場しました。これらは、ビルド時に CSS ファイルを生成することでスタイルを当てるため、パフォーマンスが改善されるとのことです。動的なスタイリングの際は、インラインスタイルを使用するようです。( 参考: Why We're Breaking Up with CSS-in-JS )
更に、Stitches というライブラリは「CSS-in-JS with near-zero runtime」を謳っており、じわじわ人気が出てきてるようです。
styled-components、Emotion 以外の CSS-in-JS ライブラリについては未検証なので、本記事では詳細は割愛しますが、以下の記事などが参考になると思います。個人的には Stitches が特に気になっているので、近々使ってみようと思ってます。
参考
- Why We're Breaking Up with CSS-in-JS
- CSS in JSとは
- 【脱ランタイムCSS in JS】styled-componentsを別のCSS in JSに自動置換するCLIツールの開発
- ZOZOTOWN Webフロントエンドリプレイスにおける CSS in JS の技術選定で Emotion を選定した話
CSS Modulesのメンテナンス状況について
CSS Modules について調べる中で、css-loaderのメンテナーが GitHub の issue 上のコメント で、「近い将来に CSS Module を deprecated にしたい」と発言してることから、CSS Modules を新規採用するのはリスクが高いと指摘している記事をいくつか拝見しました。
この問題に関しては、CSS Modulesの歴史、現在、これから でとても詳しく解説されているので是非読んで頂きたいですが、結論から言うと、現時点では CSS Module を新規採用しても問題ないと思ってます。個人的には、Next.js が引き続き CSS Moudles をビルドインサポートしている上で、トップページ(以下の画像)で紹介していることも安心材料の1つです。
おわりに
Ant Design との共存を前提としつつ、ローカルスコープでスタイリングする手法について色々と検証しました。その結果、CSS Modules、styled-components、Emotion を用いれば、思ったより簡単に実現できることがわかりました。余談ですが、私が担当しているプロジェクトにおいては、CSS Modules が 1番適していると思ってます。理由は以下の通りです。
- SCSS はデザイナーさんが書くこともあるので、SCSS ファイルに記述できた方が移行がスムーズ
- 新たにライブラリを導入しなくても良い
一方で、個人開発で使うなら、Emotion の css Prop かなって思いました。理由は以下の通りです。
- 全てJavaScriptで完結する方が開発体験が良さそう
- アプリの規模も小規模なはずなので、パフォーマンスは問題にならなそう
- Styled Component を作るより、css Props を渡す形の方がスッキリしていて好み
最後に、本記事の説明に間違いがあったり、React + Ant Design なプロジェクトのスタイリング手法について、もっとこんな方法もあるよ!などがあれば、ぜひコメントなどで教えて頂けるとありがたいです!私自身も引き続き色々試しつつ、何か学びがあったら本記事を修正したり、別途記事を書こうと思います。
Discussion