🐭

【React/Vue.js】コンポーネント指向と好相性なCSS Modulesを用いたCSS設計について|Offers Tech Blog

2022/08/04に公開

概要

こんにちは、Offers を運営している株式会社 overflow の Software Engineer(主戦場はフロントエンド)の Kazuya です。今回は、CSS Modules を用いたコンポーネントの CSS 設計について紹介します。

コンポーネントを作成する際に、どのような CSS 設計にすればいいのか悩んだ方も多いのではないでしょうか。(筆者はよく探求の旅に出ています)本記事では、昨今フロントエンド開発で採用されるケースが増えている「CSS Modules」を用いた CSS 設計を実装例を元に解説していますので、ぜひ参考にしてもらえればと思います。

おすすめの記事

https://zenn.dev/offers/articles/20220523-component-design-best-practice
https://zenn.dev/offers/articles/20220613-component-props-design

はじめに

本記事では、CSS Modules を用いたコンポーネントの CSS 設計について紹介します。基本的に他のフレームワークや言語でも活用できますが、チームメンバーのスキルアセット、要件定義など様々な要因で本記事の内容とマッチしない場合があります。今回は設計の一例であることをご理解の上、参考にしていただけると幸いです。

CSS Modules とは?

CSS Modules とは、「コンポーネント毎に CSS をローカルスコープ化する技術のこと」です。基本的に JavaScript で CSS をインポートしてクラス名を付与します。React のフレームワーク Next.js が推奨していることもあり、再度注目をされ始めています。React の JSX(TSX)だけでなく、Vue.js などにも導入しやすいことから、導入されるケースが増えてきているように感じます。

Scoped CSS との違い

Vue.js を触れたことがある人なら見たことがあると思いますが、ある特定の範囲にのみ CSS を適用できる「Scoped CSS」という技術が別に存在しています。Scope 化させるという点が、CSS Modules と類似していますが、実現させる仕組みが異なります。文字で説明するより、実際に見てもらったほうが理解しやすいと思いますので、コードベースで解説していきます。

CSS Modules

<div class="components_container_1234a">
     
  <h1 class="components_title_5678b">タイトル</h1>
</div>
コンパイル後のCSS
.components_container_1234a {
}
.components_title_5678b {
}

CSS Modules は、コンパイル時に「クラス毎にユニークな文字列を付与」することで、スコープ化を実現させています。コンポーネント毎にコンパイルされるため、CSS の影響範囲が閉じている状態になります。そのため、親コンポーネントと子コンポーネントに同名のクラスがが存在していても、お互いに干渉することはありません。
ただし、クラス名の付与を JavaScript 側で行うため、やや書き方が従来に比べて特殊になります。

Scoped CSS

<div data-hoge-1234a class="container">
     
  <h1 data-hoge-1234a class="title">タイトル</h1>
</div>
コンパイル後のCSS
div[data-hoge-1234a].container {
}
div[data-hoge-1234a].title {
}

対して、Scoped CSS は「カスタムデータ属性を付与」してスコープ化させています。付与されたカスタムデータ属性は、コンポーネント毎にユニークになるため、同名のクラスが存在していても影響を受けません。また、クラス名の付与も従来の CSS と同じような書き方ができるため、学習/導入コストは低めの印象です。

ただし、CSS Modules と異なり、親コンポーネントのスタイルが子コンポーネントに継承されてしまうため、親と子で同名のクラス名やセレクタが存在していると意図しないスタイルになる可能性があります。そのため、ルート要素にクラス名を付与、セレクタの使用をできるだけ控えるなどの CSS 設計に工夫が必要になります。

<div data-hoge-1234a class="container">
   //親コンポーネント    
  <h1 data-hoge-1234a data-foo-5678b class="container">タイトル</h1>
  //子コンポーネント
</div>
親コンポーネント
div[data-hoge-12345a].container {}
子コンポーネント
div[data-foo-5678b].container {}

上記のように子コンポーネント側にも親コンポーネントのカスタムデータ属性が付与されてしまうため、結果的に親コンポーネントの影響を受けてしまいます。以上のことから、Scoped CSS は、外部からの影響を受けやすい仕様のため、導入時には併せて CSS 設計についてもしっかり検討する必要があります。

CSS Modules のメリット/デメリット

メリット

  • 従来の CSS と書き方が変わらないため、学習コストが低め
  • コンポーネント毎にスコープ化されるため、外部からの干渉を受けづらい
  • モダンなフロントエンド環境で導入しやすい
  • CSS in JS に比べてパフォーマンス周りで優れている

デメリット

  • CSS ファイルと JS ファイルで分離してしまうため、管理コストが高め
  • CSS in JS と異なり、JavaScript で動的に CSS を操作しづらい
  • 将来的に非推奨になるリスクがある

CSS Modules の仕組み

CSS Modules は、コンポーネント毎に CSS ファイルが別々にコンパイルされるため、BEM などの複雑なクラスセレクタを使わず、シンプルなクラスセレクタを使うことができます。これにより、グローバルスタイルが汚染されないため、意図しないスタイルが適用されるリスクが下がります。
どのようにして実現されているかは、後述でコードベースで解説していきます。

CSS Modules 導入前

従来の HTML と CSS を用いたコンポーネントに対してスタイルを適用させる場合、以下のような BEM などの CSS 設計規則を導入することが多いと思います。

<button class="article-button article-button--primary">ボタン</button>
.article-button {
}
.article-button--disabled {
}
.article-button--primary {
}
.article-button--secondary {
}

上記のコードは、BEM で定められた規則に基づいて CSS のクラス名を命名しているため、クラス名からコンポーネントの役割、各スタイルの範囲が明確になっており、保守性が極めて高めです。ただし、グローバルスタイルに同名のクラス名が存在してしまうと、干渉して意図しないスタイルが反映される問題があるため、命名に気を使う必要がありました。(筆者的には、BEM の長くなりがちな命名規則にも課題感を持っていました)

CSS Modules 導入後

CSS Modules の場合は、スコープ化されるため BEM のような複雑なクラスセレクタではなく、より簡略的な名前で問題ありません。従来と異なるのは、JavaScript からファイルを読み込んでクラス名を付与する点です。これにより、ユニークなクラス名がコンパイル時に自動生成されて、付与されるようになります。

import styles from "./button.scss";

export const Button: React.FC = ({ children }) => {
  return <button className={styles.default}>{children}</button>;
};
button.scss
.default {}
.disabled {}
.primary {}
.secondary {}

上記のようにコードでは、JavaScript 内で CSS をインポートしてそれをクラスに付与しています。これをコンパイルすると以下のようになります。

コンパイル後
<button class="components_button__default__1234a">ボタン</button>

CSS 設計

命名規則

CSS Modules は、コンポーネント毎に CSS が閉じるため、ファイル内で同名のクラス名を付与しなければ、干渉することはありません。従来であれば、BEM のような CSS 設計規則を採用していましたが、CSS Modules では無理して導入する必要はありません。個人的には、CSS Modules における命名は「できるだけ端的且つ名詞」を意識しています。直感的かつメンバー間で共通認識を持ちやすいため、導入コストと運用コストをバランス良く維持できます。

スタイルの再利用

CSS Modules には、composes と呼ばれる機能が実装されています。composes は、簡単に説明すると「全てのスタイルで共通の状態を共有する機能」です。これだけだと理解しづらいので、コードベースで解説していきます。

button.scss
.base {}  /* 全てのスタイルで共通 */
.default {
  composes: base;
}
.disabled {
  composes: base;
}
.primary {
  composes: base;
}
.secondary {
  composes: base;
}

composes でクラスを指定することで、「指定したクラスの全スタイルを含む」状態にできます。この機能を使うことで、JavaScript によるクラス名制御をより簡略化できます。

Sass でも類似した @extends という機能がありますが、仕組みが異なります。この辺について深堀りしてしまうと記事が長くなってしまうため、今回は割愛させてもらいますが、いずれ別記事で詳しく解説できればと思います。

スタイルの共有

アプリケーション内で扱う色やスペースなど共通化させたい場合、variables.scss に CSS 変数や関数などを定義して用いることが多いと思います。シンプルで扱いやすいですが、依存関係が分かりづらいという課題がありました。CSS Modules では、前述した composes を使用することで、指定したファイルから特定のクラスだけ参照するため、どのファイルに依存をしているか把握しやすくなります。

palettes.scss
.primary {
  color: black;
}
.secondary {
  color: white;
}
コンパイル前
.primary {
  composes: primary from "./palettes.scss";
}
コンパイル後
.components_button__primary__1234a {
  color: black;
}

パフォーマンスと可視性に優れていますが、やや見慣れない書き方になってしまうため、個人的には従来の CSS 変数や関数をグローバルに配置する手法も使い慣れているという観点から有用だと考えています。よってここに関しては、メンバーのスキルや学習コストなど様々な要因を加味して導入を検討してもらうと良いと思います。

具体例

以上を踏まえて、最後にボタンのコンポーネントを具体例に出して紹介していきます。今回は、React で書いていますが、基本となる部分はどの言語でも変わらないため、参考にできると思います。

今回はステータスによってボタンのスタイルが変化するコンポーネントを作成していきます。実際には他にも Props に含めるべき要素がありますが、今回は CSS Modules の使い方を紹介するのが目的のため、割愛させてもらいます。

import styles from "./button.scss";

interface Props {
  status?: string;
}

export const Button: React.FC<Props> = (props) => {
  const { status, children } = props;

  const className = () => {
    switch (status) {
      case "disabled":
        return styles.disabled;
      case "error":
        return styles.error;
      default:
        return styles.default;
    }
  };
  return <button className={className()}>{children}</button>;
};

CSS Modules は、JavaScript 内で CSS をインポートしてクラス名を付与する必要があるため、まず CSS をインポートしています。その後は、Props の status の値に応じて付与するクラスを変更しています。(今回は Switch 文で書いていますが普通に If 文でも問題ありません)

background.scss
.primary {
  background: blue;
}
.disabled {
  background: grey;
}
.danger {
  background: red;
}
button.scss
.base {
  border-radius: 4px;
  cursor: pointer;
}
.default {
  composes: base;
  composes: primary from "./background.css";
}
.disabled {
  composes: base;
  composes: disabled from "./background.css";
}
.error {
  composes: base;
  composes: danger from "./background.css";
}

まず、全クラスで共通のスタイルを base というクラスに書きます。他のクラスは、composesbase を呼び出して装飾していくイメージです。このような構成にすることで JavaScript 側で付与するクラスを最小限に抑えることができ、無駄なクラスを作る必要もなくなります。

次に各ボタンの背景色を、defaultdisablederror それぞれのクラス毎に定義していますが、ここでも composes を活用しています。別ファイル background.scss で定義したクラスをインポートしています。グローバルに配置した CSS 変数を用いる方法もありますが、この方法だとデータソースが明確になり、依存関係を確認しやすい状態にできます。

まとめ

今回はコンポーネント指向と好相性な CSS Modules を用いた CSS 設計について紹介しました。CSS 設計のトレンドは目まぐるしく変化しているため、キャッチアップするのが大変だったり、結局どれを採用すればいいのか判断が難しいと思います。本記事で紹介した内容が、少しでもお役に立てれば嬉しいです。

本記事を最後まで読んで頂き、ありがとうございました。「こんな記事を書いてほしい!」などありましたらコメントいただけると幸いです。

おまけ

弊社が CSS Modules を選定するまでの経緯

課題

現在自身が担当している新規プロダクトでは、自分がアサインするまでは前述で解説した Scoped CSS を採用していました。ただ、開発を進めていくにつれて親コンポーネントやグローバルスタイルによる意図しないスタイル
の適応という問題がでてきました。そこで今後、中長期的な視点で開発をしていくにあたり、この課題は無視できないと判断し、CSS の技術選定から見直すことにしました。

選定における基準

CSS の技術選定を見直すにあたり、重視する項目を洗い出してみました。

  1. 運用コストと保守性のバランスに優れたもの
  2. 再利用性がある程度担保できるもの
  3. デザイナーやバックエンドエンジニアなど多くの人がキャッチアップしやすいもの
  4. フレームワークの変更などをした際に移行コストができるだけ低いもの
  5. 中長期的な運用(最低でも 2 年以上)ができそうなもの。または将来的にトレンドになりそうなもの

導入までの経緯

前述の項目をあげた時点で、自分の中では 3 つぐらいに絞られていました。これは過去に様々な新規プロダクト開発に携わってきた経験というところが大きいです。

  • styled-components
  • emotion
  • CSS Modules

以上の 3 つからどれにしようか選定を始めました。3 つの技術をそれぞれの基準に当てはめて見たところ以下のような感じになりました。

styled-components emotion CSS Modules
運用コストと保守性のバランスに優れたもの
再利用性がある程度担保できるもの
デザイナーやバックエンドエンジニアなど多くの人がキャッチアップしやすいもの
フレームワークの変更などをした際に移行コストができるだけ低いもの
中長期的な運用(最低でも 2 年以上)ができそうなもの。または将来的にトレンドになりそうなもの

以上のことを踏まえて総合的に判断したところ、CSS Modules を採用することにしました。個人的には、今後デザインシステムを構築していくにあたり、デザイナーとの協業は必須であるという認識だったので、そこを特に重要視して判断しました。

これ以上書くと長くなってしまい、おまけが本編になりかねないので、今回はこの辺で締めさせてもらえればと思います。今回触れた、新規プロダクトにおける技術選定や開発環境の解説に関しては、将来的に記事を書く予定ですので、気長にお待ちいただければと思います。

関連記事

https://zenn.dev/offers/articles/20220523-component-design-best-practice
https://zenn.dev/offers/articles/20220613-component-props-design
https://zenn.dev/offers/articles/20220519-thinking-about-dark-mode

Offers Tech Blog

Discussion