定義
サイトの色・間隔・角の半径といったデザイン値を一箇所で管理し、ライト/ダークモードやブランド別テーマを「値だけ変えて」切り替えたいときに使う方法である。そのために値をCSSカスタムプロパティ(--tokenの形で宣言して再利用する変数)に切り出し、モダンCSSの3つの道具で適用ルールを分ける: どのルールが勝つかは@layer(カスケードレイヤー — CSSルールを順序ある束に分けて優先順位を決める機能)、どこに適用されるかは@scope(スタイルが効くDOM範囲を限定する機能)、モードに応じてどの値かはlight-dark()(ライト/ダークに応じて2つの値のうち一方を選ぶCSS関数)。すると、テーマ切り替えはトークン値を変えるだけの作業に縮む。
なぜ必要か
テーマ色をあちこちのクラス・インラインスタイルに直接埋め込むと、「この色がなぜ変わらないのか」といった問題が生じる。同じ要素に複数のルールがかかって互いに勝とうと競い(優先順位の衝突)、同じ値が複数箇所で重複し、ダークモード用の分岐が各所で増える。原因はたいてい詳細度(specificity — .a .bのようにセレクタが具体的なほど勝つCSSの優先順位ルール)争いに埋もれて見つけにくい。「勝ったルールは何か」「どこまで適用されるか」「このモードの値は何か」という3つの問いをそれぞれ@layer·@scope·light-dark()に分離すると、1つずつ別々に答えられ、デバッグと拡張が容易になる。
動作原理
| 道具 | 役割 | 注意点 |
|---|---|---|
--token: value | 継承されるトークン。値の差し替えで伝播 | 継承・カスケードに従うため適用境界が重要 |
@layer reset, base, theme | レイヤー順で優先順位を決定 | 後のレイヤーが詳細度と無関係に勝つ |
@scope (root) to (limit) | トークンをサブツリーに限定 | to ([data-theme])下限で入れ子テーマを隔離 |
light-dark(l, d) | color-schemeに応じて2値から選択 | [light, dark]の対を1プロパティにエンコード |
@layerで宣言したレイヤーは宣言順に優先順位が決まる。後から来たレイヤーが常に勝つため、reset → base → themeの順ならテーマレイヤーがベースを常に上書きする。このとき個々のルールの詳細度はレイヤー間の比較に影響しない。
実務での適用
ランタイムに注入するCSSとビルド時に生成するCSSが別々に作られると、2つの結果がずれてテーマが壊れる。2つの消費経路が1つの生成器を単一の真実の源として共有するようにし(single-source-of-truth-content-metadata)、成果物は一時ファイルに書いてから名前を変える原子的書き込みで、半分だけ生成された状態を防ぐ。
トレードオフ
- 利点: 優先順位・範囲・モードが分離されデバッグ・拡張が容易で、詳細度戦争が消える。トークン値を一箇所変えるだけで全体が切り替わる。
- コスト:
@layer/@scope/light-dark()は比較的新しい機能なので、旧ブラウザ対応が必要ならフォールバックのコストがかかる。レイヤー順の設計を最初にきちんと決める必要がある。
使ってはいけない場合
- テーマが1つだけでダークモードもない単純なサイト(レイヤー・スコープ導入の利得が少ない)。
- レガシーブラウザの比率が高く、モダンCSS機能へのフォールバックコストが過大な場合。
よくある間違い
light-dark()を使いながらcolor-schemeを設定せず、常に最初の値だけが出る。- 詳細度を上げてテーマを強制しようとしてレイヤーモデルと衝突する(レイヤー間の優先順位は詳細度で覆せない)。
- ランタイム・ビルドのCSS生成器を二元化して2つの結果が微妙に異なる。
@scopeの下限(to (...))を欠いて意図より広い範囲にトークンが漏れる。
関連概念
- single-source-of-truth-content-metadata — 同一の成果物を複数経路が消費するとき生成ロジックを単一の源に置く原則。