mizdra's blog

ぽよぐらみんぐ

CSS Modules の拡張構文について

CSS Modules は、CSS をローカルスコープ化する仕組み。*.module.css に CSS を記述すると、bundler がクラスセレクターなどをユニークなものへと変換してくれる。クラスセレクターなどが *.module.css ファイルごとに異なる名前に変換され、擬似的にローカルスコープ化が実現される。

developer.hatenastaff.com

CSS Modules では、基本的には CSS の標準の構文をそのまま利用する。しかし、一部 CSS Modules 独自の構文がある。実際どのようなものがあるのかというのを、紹介する。

CSS Modules の公式ドキュメント (仕様) のリンクを貼りつつ紹介するが、公式ドキュメントの仕様は非常に緩く書かれているので注意すること。真の仕様を把握するには、postcss-modules などの実装も必ず見るように。

:local(), :global() 擬似クラス

擬似クラスの一種で、クラスセレクターをローカルスコープ化するのか、グローバルスコープ化するのかを切り替えるための構文。以下の公式ドキュメントで言及されてる。

以下のようにしてクラスセレクターを囲むと、そのスコープモードを変更できる。

:local(.button) {/* ... */}
/* Bootstrap の .btn-primary を使いたいので、:global(...) で囲む */
:global(.btn-primary) {/* ... */}

:global(...) は UI ライブラリ側で決め打ちされてるクラス名をそのまま使いたいなど、ローカルスコープ化されると困るときによく使う。一方 :local(...) については、基本的に付けても意味がない。通常 CSS Modules ではデフォルトでローカルスコープであるため。しかし css-loader ではデフォルトのスコープモードを変更することができる。デフォルトのスコープモードが "global" の場合、ローカルスコープ化するために :local(...) が使われる。

とはいえデフォルトのスコープモードを "global" にすることまずないので、:local(...) の出番はほぼないと思って良い。

参考情報:

:local, :global 擬似クラス

さっきのやつの括弧 () が無いバージョン。以下の公式ドキュメントで言及されてる。

括弧有りとの違いは、スコープモードの変更範囲である。括弧有りでは「括弧の中のものだけ」が、括弧無しでは「疑似クラスに続くもの全て」がスコープの変更対象となる。

/* a_1 はローカル、a_2, a_3 はグローバル */
.a_1 :global .a_2 .a_3 {}

/**
 * :local で途中でスコープモードを変更可能。
 * b_1, b_4 はローカル、b_2, b_3 はグローバル。
 */
.b_1 :global .b_2 .b_3 :local b_4 {}

...というのが表面的な仕様なのだけど、(CSS Modules の主要な実装である) postcss-modules ではもっと複雑な仕様になってる。セレクターリストや擬似クラスと組み合わせると、非常に奇妙な動作をする。ここで紹介するには難しすぎるので、興味ある人は css-modules-kit のテストケースにその挙動の説明が書いてあるので、それを見てほしい。

参考情報:

@value アットルール

アットルールの一種で、変数を宣言するための構文。以下の公式ドキュメントで言及されてる。

以下のようにして使う。

/* Header.module.css */
@value red: #FF0000;
@value headerHeight: 30px;

.header {
  height: headerHeight;
  background: red;
}

Value/Variables といっても bundle 時に展開されるもので、「bundle 時定数」「コンパイル時定数」などと言ったほうがわかりやすいかも。Sass の Variables にかなり近い。

ただし Sass の Variables と違い、変数が *.module.css から export される。以下のようにして、コンポーネントファイルから変数を参照できる。export される型は常に string

// Header.tsx
import styles from './Header.module.css';

styles.red; // '#FF0000'
styles.headerHeight; // '30px'

また、他の *.module.css から変数を import することもできる。

/* common.module.css */
@value red: #FF0000;
@value white: #FFFFFF;
/* Header.module.css */
@value red, white from './common.module.css';
@value headerHeight: 30px;

.header {
  height: headerHeight;
  background: red;
  color: white;
}

参考情報:

composes プロパティ

mixin みたいなことをするやつ。以下の公式ドキュメントで言及されてる。

あるルールセットで定義されてるプロパティを、別のルールセットに取り込める。

/* https://github.com/css-modules/css-modules/blob/master/docs/composition.md より引用 */
.className {
  color: green;
  background: red;
}

.otherClassName {
  composes: className;
  color: yellow;
}

上記のコードは以下と同じ意味になる。

.className {
  color: green;
  background: red;
}

.otherClassName {
  background: red;
  color: yellow;
}

複数のルールセットをまとめて取り込んだり、複数の他のファイルのものを取り込むこともできる。

.a_1 {
  color: green;
  background: red;
}
.a_2 {
  composes: common_1 from './common.module.css';
}
.a_3 {
  composes: a_1 a_2;
}

composes: global_1 from global; みたいにグローバルから取り込む機能もあるらしいが、正直どういう挙動をするのか id:mizdra はよくわかってない。どのスタイルシートから取り込まれるの?

@keyframes :local(<custom-ident>), @keyframes :global(<custom-ident>)

実は CSS Modules は、デフォルトで @keyframes で定義した名前をローカルスコープ化する。ローカルスコープ化されるということは、それをグローバルスコープ化したい時もある訳で、そのための構文がこれ。CSS 標準では @keyframes <custom-ident> だけど、CSS Modules ではそれを拡張して :local():global() で囲めるようにしてる。

公式ドキュメントでの言及は僅かで、以下で @keyframes :global(<custom-ident>) という構文があることが匂わされてるくらい。

@keyframes :global(<custom-ident>) だけでなく、デフォルトのスコープモードが "global" の時のために、@keyframes :local(<custom-ident>) も用意されてる。

参考情報:

animation, animation-name プロパティ

@keyframes で定義した名前は animation-name と、その一括指定プロパティである animation からも参照できるので、それも拡張されてる。公式ドキュメントでの言及はちょっとだけ。

@keyframes と同じく、アニメーションの名前を :local(), :global() で囲めるような拡張がされてるだけ。

var(<custom-property-name> from <string>)

実は lgithningcss では、カスタムプロパティ もローカルスコープ化される。

/* https://lightningcss.dev/css-modules.html#local-css-variables より引用 */
:root {
  --accent-color: hotpink;
}
.button {
  background: var(--accent-color);
}

加えて他の *.module.css のカスタムプロパティを参照する機能もある。そのための拡張構文が var(<custom-property-name> from <string>) である。

/* https://lightningcss.dev/css-modules.html#local-css-variables より引用・一部改変 */
.button {
  background: var(--accent-color from './vars.module.css');
}

ちなみにこの機能は lightningcss 独自のもので、postcss-modules などの他の実装ではサポートされてない。CSS Modules の公式ドキュメントでも一切言及がない。

:import, :export

ICSS で使われる構文として、:import, :export がある。ICSS は postcss-modules 内で使われる中間表現で、import/export するクラスセレクターなどが明示的に記述される。その明示的な記述を行うための拡張構文がこれ。

詳しくは以下のリポジトリの README を見ると雰囲気がわかる。

まず人間が手で書くことはないので、忘れて良い。

拡張構文の問題点

これらの拡張構文だが、問題点が色々ある。例えば @keyframes :global(.foo) が奇妙という点。セレクターを書く場所じゃないのに、擬似クラスの構文 (:global()) が使われていて変だと思う。

xxx from './vars.module.css' のような構文が animation プロパティなどで使えなくて、一貫性がないのも微妙。そもそも仮に animation: slide-in from './vars.module.css' と書けたとして、どれがアニメーションの名前で、easing-function の名前なのか定まらなくて、破綻してしまうと思う。

拡張構文があることで将来の CSS の新構文と衝突してしまう恐れがあるのも問題だと思う。@value アットルールや composes プロパティなど全く独自のものならともかく、var(), @keyframes, animation-name など既存の構文を拡張しているものは、結構危ういと思う。できるだけ CSS の標準の構文を使用することで CSS の仕様変更に追従しやすい、というのが CSS Modules の良さだと個人的には思ってて、そこが損なわれてるのが勿体ないと思う。

理想的にはどうあるべきか

:global(), xxx from global, xxx from <string> といった記法を廃止すべきだと思う。代わりに、以下のように「ある識別子をグローバルスコープにするのか、他のファイルから import してくるのか」を示せる構文を追加する。

@cm-global btn-primary, slide-in;
@cm-import red, flex from './common.module.css';

/* a_1 はローカル, btn-primary はグローバル */
.a_1 .btn-primary {}

/* slide-in はグローバル */
@keyframes slide-in {}

/* a_2 はローカル, red は common.module.css のもの */
.a_2 .red {}

.a_3 {
  /* flex は common.module.css のもの */
  composes: flex;
}

一応似たような提案が以下の Issue で行われているようだった。議論が進むと良いなー。

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.