CSS Modulesの歴史、現在、これから

マンガメディア開発チームの id:mizdra です。半年ほど前から「フロントエンドエキスパート」という肩書きをもらい、社内でフロントエンドの啓蒙活動をしています。具体的にどんな活動をしているかについては、社内のポッドキャストで少し話しましたので、興味があれば聞いてみてください。

developer.hatenastaff.com

最近、私はReactを採用する社内プロダクトでのCSSの書き方を検討していました。最終的にそのプロダクトでは、CSS Modulesを採用するに至りました。しかしその過程で、CSS Modulesのメンテナンス体制に対して懸念があり、将来的な存続を危ぶむ声が界隈にあることを知りました。

ただし、実際にメンテナンス体制について調べてみたところ、万全ではないものの引き続きメンテナンスがされていて、使用もできることが分かりました。そこで、今回はCSS Modulesについて、その歴史から振り返りつつ、現状のメンテナンス体制、そして将来まで、一通りまとめてみようと思います。

CSS Modulesとは

CSS Modulesとは、CSSのグローバルスコープ汚染問題を回避するための仕組みです。グローバルスコープ汚染問題を回避する技術には、CSS-in-JSなどさまざまなものがあり、CSS Modulesもその1つです。

webpack(厳密にはwebpackのloaderであるcss-loaderの実装)viteといったbundlerごとに、CSS ModulesモードでCSSファイルをbundleするモードが用意されています。たいていのbundlerでは、CSSファイルの名前を*.module.cssにすると、自動でCSS ModulesモードをONにしてbundleします。

CSS Modulesを利用するコードの例

例えば以下のように、JavaScriptファイルからCSSファイルをimportするようなコードがあったとします。

/* src/Counter.module.css */
.count {
  color: red;
}
@media (max-width: 12450px) {
  .count {
    font-size: 1.5rem;
  }
}
.button {
  border-radius: 50%;
}
.button:hover {
  color: red;
}
// src/Counter.tsx
import styles from './Counter.module.css';

function Counter() {
  const [count, setCount] = useState(0);
  const increment = useCallback(() => setCount((count) => count + 1), []);
  return (
    <div>
      <span className={styles.count}>
        {count}
      </span>
      <button onClick={increment} className={styles.button}>
        increment
      </button>
    </div>
  );
}

これを以下のようなwebpack.config.jsでbundleすると、

// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
    ],
  },
};

以下のようなCSSファイルが出力されます実際の出力結果はこちら

/*!*****************************************************************************************************************************!*\
  !*** css ./node_modules/.pnpm/css-loader@6.7.1_webpack@5.74.0/node_modules/css-loader/dist/cjs.js!./src/Counter.module.css ***!
  \*****************************************************************************************************************************/
.DkhXIKFSVtrX837kPko3 {
  color: red;
}
@media (max-width: 12450px) {
  .DkhXIKFSVtrX837kPko3 {
    font-size: 1.5rem;
  }
}
.DO8HfK3ZS4ejgwpTbHe3 {
  border-radius: 50%;
}
.DO8HfK3ZS4ejgwpTbHe3:hover {
  color: red;
}

このようにCSS Modulesでは、クラスセレクターがユニークになるように変換することで、クラスセレクター同士が干渉しないようにします(擬似的なローカルスコープを実現します)。これによってグローバルスコープ汚染問題を回避します。

また、ここでは詳しく解説しませんが、Sassのmixinに相当するCompositionや、コンパイル時定数に相当するValuesといった機能もあります(このあたりはSassやLessの機能で代替できるので、ちょっとしたおまけ機能という認識でかまいません)

他のソリューションと比較したメリット

CSS Modulesは(Compositionなどちょっとした拡張はあるものの)基本的にクラスセレクターのみに手を加える仕組みのため、他のソリューションと比較しても挙動が理解しやすいです。また、馴染みがある書き方でスタイルを書けるので、学習コストも低いです。メディアクエリなど、CSSの標準機能も自然な形で使えます。

他のソリューションとの比較についてもっと知りたい方は、以下の記事などを読んでみてください。

postd.cc

CSS Modulesの仕様

CSS Modulesの仕様は、以下のGitHubリポジトリで管理されています。

css-modules/css-modules

README.mdに大枠の仕様が書かれていますが、docs/ディレクトリ配下にもドキュメントがいくつかあり、こちらにも仕様が書かれています。

css-modules/docs at master · css-modules/css-modules

仕様は1つだが実装は複数

この仕様をもとに、さまざまなツール(css-loaderやvite)がCSS Modulesを実装しています。仕様はただ1つですが、実装は複数あります。実装と仕様が一体になっている代替手法(styled-componentsやstyled-jsx)と比較すると、ここがCSS Modulesの大きな特徴です。

https://cdn.blog.st-hatena.com/files/12704346814673975483/4207112889913647898
CSS Modules の仕様と実装の関係

かなり緩く書かれた仕様

W3Cが公開しているCSSの仕様書を読んだことのある方なら分かると思うのですが、CSSの仕様書に対してCSS Modulesの仕様書はかなり緩く書かれています。

こういった仕様を厳密に書こうとするのなら、CSSの仕様書にも適時リンクしながら、CSS仕様のどこをどう拡張するかを説明しなければならないでしょう*1。しかし、実際のCSS Modulesの仕様書はそのようになっていません。

また、どこまでを仕様がカバーし、どこからを実装がカバーするのか、その線引きも明示されていません。読者が、CSSの仕様の用語や概念にリンクさせたり、仕様の範囲を想像したりしながら、読む必要があります。

CSS Modulesの成り立ち

もともとCSS Modulesは、css-loaderの独自拡張の機能でした。@sokra(webpackのCore Teamの一人)によって実装され、2015年4月にcss-loader 0.11.0で実験的機能としてリリースされました。

export placeholder · webpack-contrib/css-loader@d2c9c25

この当時はまだ「Placeholders」と呼ばれていて、仕様も実装から分離していませんでした。記法も今と異なっていて、.[className] {...}のように角括弧で囲む必要がありました。

その後、2015年10月にかけて記法の微調整や新機能の実装、名称の変更などが行われました。仕様書のHistoryを見ると、初期の実装は@sokra氏によってなされ、仕様書は@sokra氏のほか@markdalgleish氏と@geelen氏によって作られています。おそらくこの3人が中心となって、今のCSS Modulesの仕様が作られたのだと思われます。

詳しいタイムライン(2015年4月〜10月)

  • 2015/4/22: export placeholder
    • 初実装
  • 2015/4/25: renamed placeholders to locals
    • .[className]から.local[className]に記法が変更
    • 機能の名称が「Local scope」に変更
  • 2015/5/21: new :local syntax
    • .local[className]から:local(.className)に記法が変更
    • 実験的機能ではなくなる
    • :local(.foo):extends(.bar from "./file.css") {...}という、Compositionの前身となる機能が実装される(実験的)
  • 2015/5/24: module mode [breaking change]
    • 機能の名称が「CSS Modules mode」に変更
    • :local(.className) {...}.className {...}と省略できるように
    • Inheritingがプロパティを使った記法に変更(.foo { extends: bar; }
  • 2015/6/18: Use PostCSS
    • CSS Modulesの仕様がcss-modules/css-modulesへと切り出され、仕様についてはそちらを参照する形に
    • PostCSSを使った実装に変更された
    • Inheritingの記法が.foo { extends: bar; }から.foo { composes: bar; }に変更される(今の形になる)
    • Inheritingが実験的機能ではなくなる
      • 名称も「Composition」に変更
  • 2015/10/20: added CSS Modules values support
    • Values機能がサポートされる

関連ライブラリやツールの誕生

css-loaderにおけるCSS Modulesの実装にあたって、実装の一部が次々とライブラリへと切り出されました。また、そのライブラリを使った関連ツールも誕生しました。

ICSSによる明示的なimport/export

またオリジナルのCSS Modulesでは、クラスセレクターのexportやimportが暗黙的に行われますが、bundlerからするとやはりimport/exportは明示的になっていてほしいということで、モジュールの境界をはっきりさせる試みも行われました。

その成果がInteroperable CSS(ICSS)です。オリジナルのCSS Modulesのスーパーセットになっていて、import/exportするクラスセレクターを:import:exportといった記法で明示的にCSSファイル内に記述する仕様になっています。

css-modules/icss: Interoperable CSS — a standard for loadable, linkable CSS

このICSSを扱うライブラリも作られています。

  • postcss-modules-extract-imports
    • 暗黙的なimport(CSS Modules形式)を、明示的なimport(ICSS形式)に変換するPostCSSプラグイン
  • postcss-modules-scope
    • 暗黙的なexport(CSS Modules形式)を、明示的なexport(ICSS形式)に変換するPostCSSプラグイン
  • icss-utils
    • ICSS形式のファイルを解析して、export/importしているクラスセレクターを抽出するライブラリ

css-loaderやviteはこれらのライブラリを利用して、CSS Modulesファイルを一度ICSS形式を経由しつつ、CSSファイルに変換しています。このように、ICSSはbundlerのための内部表現であり、一般のユーザが書いたりすることは想定されていません。一般のユーザは意識しなくてよいものです。

https://cdn.blog.st-hatena.com/files/12704346814673975483/4207112889913647901
ICSS の使われ方の図解

ライブラリやツールの依存関係

ここまでに登場したライブラリやツールについて、依存関係を図にすると以下のようになります。

https://cdn.blog.st-hatena.com/files/12704346814673975483/4207112889913647899
CSS Modules 関連ライブラリ・ツールの依存関係

CSS Modulesのメンテナンス状況

css-loaderのメンテナーが「CSS Modulesモードを将来的にdeprecatedにしたい」と意思表明をしているのを見てCSS Modulesはもう長くないのではないかとなったり、一方でVercelの中の人が「Next.jsの公式ドキュメントをCSS Modulesを推奨するように書き換えたい」というIssue*2を立てたことがあったり(実際には書き換えられなかったようですが)、CSS Modulesの先行きに関しては界隈でも揺らぎがあるようです。

実際、こうした情報を見て、CSS Modulesを採用するべきか、それとも乗り換えを検討するべきか、悩んでいる方もいるようです。私としてはこの場でCSS Modulesの是非について述べるつもりはありませんが、現時点におけるCSS Modulesのメンテナンス状況について知っておくと、悩みを解消する助けになるでしょう。

というわけで、メンテナンス状況を簡単にまとめてみます。

  • 仕様リポジトリ
    • アクティブなメンテナーがいなくなり、2016年末頃からメンテナンスが滞る
    • 代わりのメンテナーの募集がなされたが、引き継ぎが上手く行かなかったのか、現在でもメンテナンスされていない状態が続いている
  • css-loader
  • css-modules-loader-core
    • 仕様リポジトリと同時期から、メンテナンスが滞っている
    • PostCSSに依存しているが、対応はv6系まで(最新はv8系なので、メジャーバージョンで2つ前)
  • postcss-modules
    • アクティブにメンテナンスされてる
    • PostCSS 8系にも対応
  • typed-css-modules / typed-scss-modules / typed-less-modules
    • アクティブにメンテナンスされてる
    • ただし、メンテナンスの滞っているcss-modules-loader-coreに依存してる

仕様がメンテナンスされていないので、新機能を提案したり、仕様上の重大な欠陥が見つかったとき*3に仕様を修正したりするのは、難しいと思われます。とはいえ、現時点でそうした欠陥は見つかっておらず、仕様は安定しているように感じています*4

加えて、各種実装はメンテナンスされており、実装レベルの不具合は修正できる状態です。重大な欠陥が見つかったときは、各種実装ごとに修正・対応されていくのではないかと考えています。

また、これは関連ライブラリ開発者向けですが、css-modules-loader-coreライブラリはメンテナンスが滞っていて利用を避けたほうがよいと思います。postcss-modulesのgetJSONオプションで代替可能ですので、こちらを使うのがオススメです。

CSS Modulesを標準化する動きについて

CSS Modules相当の機能をCSSでカバーしようという動きがいくつかあります。

reference selector

以下のproposalがW3Cに提出されています。この提案では「reference selector」という新たなセレクターが導入されます。

[css-selectors] Reference selectors · Issue #3714 · w3c/csswg-drafts

セレクターリストに$<name>という記法で書くと、JavaScript向けに「reference」がexportされます。

/* サンプルコードは Proposal から引用 */
$foo {
  color: red;
}

exportされたreferenceは、以下のようにJavaScriptからimportできます。

// サンプルコードは Proposal から引用
import styles, { foo } from './styles.css';

styles instanceof CSSStyleSheet; // true
foo instanceof CSSReference; // true
styles.references.foo === foo; // true

referenceの実態はCSSReferenceのインスタンスです。クラス名(文字列)ではないため、要素のclass属性に渡すことはできません。代わりにProposalではcss-refs属性、cssReferencesプロパティなどを追加して、これらを使って要素とreferenceを紐付ける仕組みが提案されています。

<!-- サンプルコードは Proposal から引用 -->
<style>
$foo {color: red;}
</style>
<div css-refs="$foo"></div>
// サンプルコードは Proposal から引用
import { foo } from './styles.css';
const div = document.createElement('div');
div.cssReferences = [foo];

最近はあまり動きがないようですが、興味があれば追ってみると面白いと思います。

@scope

セレクターがマッチするスコープを制限する@scopeという機能が提案されています。これ自体はCSS Modulesを再現するために作られたわけではありませんが、応用範囲が広く、工夫するとCSS Modulesとほぼ同等のことができます。

詳しくは、以下の記事などを読んでみてください。

zenn.dev

はてな社内におけるCSS Modulesの採用状況

はてなではデザイナーがCSSを書く体制になっており、デザイナーがCSSを書きやすい形が望まれていました。そのため、新規開発・リニューアルするプロダクトでは、他のソリューションと比較して標準的なCSSに近い形でコーディングができるCSS Modulesを主に採用しています。

CSS Modulesのメンテナンス体制は気になるところですが、将来より良い代替手段が登場するまでのつなぎとしては、十分機能するだろうと考えています。

また、はてな社外の取り組みになりますが、@yoshiko_pgさんが以前「SPAにおけるCSSについてもうひとつの解」というタイトルで発表をなされています。この発表で触れられているCSS-in-JSに対する課題意識やCSS Modulesの利点が、まさにはてな社内で議論されたこと重なっていました。興味があれば、あわせて以下の発表資料を読んでみてください。

SPAにおけるCSSについて、もうひとつの解 - @yoshiko_pg

まとめ

  • CSS Modulesはグローバルスコープ汚染問題を回避するためのもの
  • ただ1つの仕様と、複数の実装(css-loader、vite)がある
  • 仕様はメンテナンスされていないが、安定している
    • 実装は引き続きメンテナンスされている
    • 何か問題があれば実装側で対応されていくはず
  • CSS Modulesの考えをCSS仕様に取り入れようと、いくつか議論が行われている

補足: CSS Module Scriptについて

CSS Modulesとよく似たものに「CSS Module Script」があります。これはW3Cで標準化されていて、すでにChromeやEdgeで実装されています。

Using CSS Module Scripts to import stylesheets

名前が似ているのでややこしいのですが、CSS Module ScriptsはCSS Modulesとは全く別物です。詳しい違いについては、以下の記事などを読んでみてください。

zenn.dev

zenn.dev

*1:例えば、JSXではECMAScriptの仕様書を参照しつつ、拡張方法を形式的に定義しています。厳密に拡張仕様を定義している非常に良い例です。https://facebook.github.io/jsx/

*2:なぜか権限不足でIssueが見れなくなっていたので、Web Archiveのリンクを貼ってます

*3:仕様に重大なバグが見つかったときや、CSSの新機能がCSS Modulesの機能と衝突したときなど。

*4:実際、https://github.com/css-modules/css-modules/issues/187に書き込まれているコメントを見ても、CSS Modulesが安定していることに言及しているものがいくつか見受けられます。