🎄

React + Ant Design なプロジェクトにおけるスタイリング手法について

2022/12/25に公開

この記事は 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-componentsEmotion  であれば目的を達成できると思いました。

本記事での前半では、そのような結論に至った理由を紹介します。後半では、デモアプリのソースコードを見ながら、CSS Modules、styled-components、Emotion について、基本的な使い方と Ant Design のスタイルの上書き方法を紹介します。

現状の問題点

プロジェクトでは以下のようにスタイルを管理してます。

  • src/sass/index.scss で全ての SCSS ファイルを @import
  • src/index.jsxsrc/sass/index.scssimport

この手法の問題点は、個々の SCSS ファイルに記述したスタイルがグローバルに影響することです。例えば、User.jsx だけで適用するために User.scss で指定した .container のスタイルが、Post.jsx などにも適用される可能性があり、管理を難しくしています。この問題を解決する方法を模索しました。

解決方法の模索

検討した選択肢

React のスタイリング手法に関する記事を読み漁り、以下の方法を検討しました。

  • BEM + SCSS
  • インラインスタイル
  • Tailwind CSS
  • CSS Modules
  • styled-components (CSS-in-JS)
  • Emotion (CSS-in-JS)

参考

選定条件

今回は以下の条件で解決方法を模索しました。

  1. SCSS からの置き換えコストが低い
  2. 一つの方法に統一できる
  3. 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 では、bodyStyleclassNamestyle プロパティが提供されてます。

  • 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

実際の動き

styling-with-antd.gif

4つのボタン、モーダルは全て同じ動きをします。
ソースコードはGitHubにあげてます。
https://github.com/daichihayasaka/styling-with-antd

ディレクトリ構成

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

https://github.com/daichihayasaka/styling-with-antd/blob/master/src/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 クラス を使用し、以下のようにすることで、スタイルの上書きを実現できました🎉

https://github.com/daichihayasaka/styling-with-antd/blob/master/src/components/CssModules/index.jsx

https://github.com/daichihayasaka/styling-with-antd/blob/master/src/components/CssModules/style.module.scss

classnames について

classnames は、条件に応じて className を設定する時などに便利なライブラリです。Reactの公式ドキュメントでも紹介されてます。 本記事の執筆時点では、Create React App で作成したプロジェクトでは、デフォルトでインストールされてます。
今回の例では、styles.modal は常に適用され、styles.fullscreen は、isFullscreentrue の時のみ適用されます。このように 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 のスタイルを上書き

以下のようにすることで、スタイルの上書きを実現できました🎉

https://github.com/daichihayasaka/styling-with-antd/blob/master/src/components/StyledComponents/index.jsx

https://github.com/daichihayasaka/styling-with-antd/blob/master/src/components/StyledComponents/style.js

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 PropStyled 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');
}

引用:https://ja.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#whats-different-in-the-new-transform

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' });
}

引用:https://ja.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#whats-different-in-the-new-transform

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 */ を記述する

参考

基本の形(既存の 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 編】

以下のようにすることで、スタイルの上書きを実現できました🎉

https://github.com/daichihayasaka/styling-with-antd/blob/master/src/components/EmotionReact/index.jsx

https://github.com/daichihayasaka/styling-with-antd/blob/6a029041c8cd38a83b94cb95547b7436128c3d76/src/components/EmotionReact/style.js#L1-L41

基本的な使い方【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 編】

一応試しましたが、本当に全く同じでした!

https://github.com/daichihayasaka/styling-with-antd/blob/master/src/components/EmotionStyled/index.jsx

https://github.com/daichihayasaka/styling-with-antd/blob/master/src/components/EmotionStyled/style.js

余談

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」を謳う linariavanilla-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 が特に気になっているので、近々使ってみようと思ってます。

参考

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