CSS Layersは2022年に主要なモダンブラウザでサポートされた機能で、CSSのカスケードを「レイヤー」という単位で管理できる仕組みです。理論的には詳細度の悩みを根本から解決してくれるはずでしたが、実際にプロジェクトに導入してみると、理想と現実の間にいくつもの壁がありました。

最終的に自分が行き着いたのは、Layersそのものをやめて、:is() / :where() でセレクタの詳細度自体を設計するという別解でした。この記事では、その紆余曲折を含めて整理します。


CSS Layersとは何か

Layersが解決しようとしている問題

CSS Layersは @layer ディレクティブを使って、CSSのスタイルを階層的に管理する仕組みです。従来の詳細度による優先順位ではなく、レイヤーの順序で明確にスタイルの適用順序を制御できます。

CSS Layersの核心は、レイヤー間の優先順位を詳細度よりも先に評価するという点にあります。@layer base の中にどれだけ詳細度の高いセレクタを書いても、後で宣言された @layer utilities の単純なクラスセレクタには必ず負けます。「レイヤーの外側の順序」と「レイヤーの中の詳細度」が多くの分離されるのが、従来のカスケードとの最大の違いです。

/* レイヤーの順序を定義(左から右へ優先度が上がる) */
@layer reset, base, components, utilities;

@layer reset {
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
}

@layer base {
  body {
    font-family: Arial, sans-serif;
    line-height: 1.6;
  }
}

@layer components {
  .button {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 4px;
  }
}

@layer utilities {
  .text-center {
    text-align: center;
  }
}

「このプロジェクトはreset→base→components→utilitiesの順で必ず上書きされる」という規律を、コード自体に明示的に埋め込めるわけです。

サブレイヤーでより細かな制御

レイヤーはドット区切りでネストでき、レイヤーの中をさらに細分化できます。

@layer reset, base, components, utilities;
@layer components.base, components.variations, components.states;

@layer components.base {
  .button {
    padding: 0.5rem 1rem;
    background: #007bff;
    color: white;
  }
}

@layer components.variations {
  .button--large {
    padding: 1rem 2rem;
    font-size: 1.2rem;
  }
}

@layer components.states {
  .button:hover {
    background: #0056b3;
  }
}

最大の注意点:レイヤー未割り当てのCSSは最優先

ここがLayers仕様で最も誤解されやすい部分です。@layer で囲まれていない通常のCSSは「名前のない最後のレイヤー」として扱われ、どのレイヤーよりも常に優先されます。つまり、自分のCSSをどれだけ精密にレイヤー分割しても、レイヤー化されていないCSSが1行でもあれば、そちらが勝ってしまうということです。


実際に使って分かった問題点

プラグインCSSとの共存問題

実プロジェクトで最大の壁になったのが、まさに前述の「レイヤー未割り当てCSSが最優先」という仕様です。

/* あなたの美しいレイヤー構造 */
@layer reset, base, components, utilities;

@layer utilities {
  .margin-0 { margin: 0; } /* 最高優先度のつもり... */
}

/* でも、プラグインの普通のCSS */
.some-plugin-class {
  margin: 20px; /* こちらが勝ってしまう! */
}

WordPressのプラグインや、外部から読み込むサードパーティライブラリのCSSは、当然ながらレイヤー化されていません。結果として、WordPressプラグインやサードパーティライブラリのCSSが、あなたの精密に設計されたレイヤー構造を無視して最優先で適用されてしまいます。

/* プラグインが定義する野蛮なリセット */
* {
  margin: 0;
  padding: 0;
  box-sizing: content-box; /* border-boxを上書きしてくる */
}

/* 自分のレイヤー内の設定は無力 */
@layer reset {
  * { box-sizing: border-box; } /* 負ける */
}

@layer utilities {
  .p-4 { padding: 1rem; } /* 負ける */
}

対処法として、wp_dequeue_style で問題のあるプラグインCSSを無効化し、自前でレイヤー化し直したCSSを読み込む、!important で局所的に勝つ、といった方法はあります。ただしこれは「プラグインCSSを多くのコントロールできる」という前提があって初めて成立する話で、プラグインの数が多い既存サイトでは現実的に運用が厳しいです。

@import のパフォーマンス問題

Layersはレイヤーごとにファイルを分割し、@import url(...) layer(...) で読み込む構成と相性が良いのですが、@import は直列読み込みのためパフォーマンスに悪影響があります。

/* パフォーマンスが悪い例 */
@import url('reset.css') layer(reset);
@import url('components.css') layer(components);
@import url('utilities.css') layer(utilities);
/* 3つのファイルを順番に読み込み = 遅い */

ビルドツールで1ファイルに結合するか、<link> タグで並列読み込みする形に変える必要があり、ここでも「Layersのために設計を変える」コストが発生します。


Layersを諦めた理由

今回扱っていたプロジェクトでは、プラグインの野蛮な * { margin: 0; padding: 0; } のようなCSSがすべてを台無しにしてしまう環境だったため、最終的にCSS Layersの本格採用を断念しました。Layersは「読み込まれるCSSが全てレイヤー管理下にある」という前提が崩れた瞬間に、理論上の利点がそのまま弱点に転じてしまいます。特にWordPressのようなプラグインエコシステムに依存するCMS環境では、この前提を担保するのが難しいというのが実感でした。


行き着いた答え:詳細度そのものを設計する

Layersを諦めたあと辿り着いたのが、:is():where() を使ってセレクタ自体の詳細度を意図的に固定する、という方向性でした。

:where() はレイヤー相当のことができる

:where() の中に何を書いても、:where() 自体の詳細度は常に0として計算されます。これを利用すると、レイヤーで実現したかった「ベースは弱く、上書きは確実に」という関係を、レイヤーという仕組みなしに表現できます。

/* .button の基本形 */
.theme-name :is(.badge, .button) {
  padding-block: 0.5em;
}

/* バリアントは :where() で包むことで詳細度を増やさない */
.theme-name :is(.badge, .button):where(.is-narrow) {
  padding-block: calc(0.5em / 1.5);
}

.is-narrow を後から付け足しても、詳細度上の優位性で上書きしているわけではなく、「同じ詳細度のまま、CSSの記述順序で確実に上書きされる」関係を作っています。これはLayersが「宣言順で優先順位を決める」のとよく似た発想です。

:is() で「基本形の詳細度」を固定する

:is() は内側のセレクタリストのうち最も詳細度が高いものがそのまま :is() 全体の詳細度として採用されます。これを利用して、「このレイヤーのセレクタは必ずこの詳細度にする」という規律を、コードの書き方そのもので強制できます。プラグインCSSのような外部要因に左右されない、CSS自身の中で完結する仕組みという点が、Layersとの大きな違いです。

実例:レイヤーごとに詳細度のレンジを決める

具体的には、ファイルの役割ごとに詳細度の目安を決めて運用しています。

レイヤー役割詳細度の目安
settings変数・トークン定義CSSの指定なし
foundationsリセット・下地0〜1
layoutsページのレイアウト1〜2
componentsバッジ・ボタンなど2〜3
utilities単一目的のユーティリティ3〜4
trumps緊急時の上書き(!important許容)

ITCSSの「逆三角形」の考え方をベースに、:is() / :where() を使ってこのレンジを実セレクタとして固定する設計です。この詳細度設計の細かい考え方や、ITCSSとFLOCSSをどう組み合わせているかは、別記事で詳しく扱います。

CSS設計|ITCSS×FLOCSSのハイブリッド構成と詳細度設計の実例 ITCSSとFLOCSSはどちらもCSS設計の代表的な手法ですが、性質も得意分野も異なります。両者の違いを整理しつつ、色トークン系ファイルの置き場に迷った実例や、`:is()` / `:where()` を使って詳細度を固定する6層構成のハイブリッド設計を紹介...  続きを読む

CSS Layersを使うべき?使わないべき?

使うことを推奨するケース

  • 新規プロジェクトで、読み込むCSSをすべて自分でコントロールできる
  • チーム開発で、CSS構造の統一が重要
  • デザインシステムを構築していて、外部CSSの混入がない

使わない方が良いケース、あるいは代替を検討すべきケース

  • 既存のWordPressサイトで複数のプラグイン・ライブラリを使用している
  • プラグインCSSを制御できない環境
  • 学習・移行コストに見合うほどの規模ではない

Layersでなくても、:is() / :where() を使った詳細度設計という選択肢があることは、特にWordPressのような環境で開発している人には知っておいてもらいたいポイントです。


まとめ

良い点

  • 詳細度の問題を根本解決できる(条件が揃えば)
  • 構造的なCSS管理が可能
  • チーム開発での保守性向上

課題

  • プラグインCSSなど、レイヤー化されていないCSSには無条件で負ける
  • @import のパフォーマンス影響
  • 学習コストと移行コスト

CSS Layersは理論的には優れた機能で、自分のCSSだけで完結する環境では今でも有力な選択肢です。ただし、WordPressのようにサードパーティCSSの混入を避けられない環境では、Layersの仕様上の弱点がそのまま運用上の弱点になります。自分の場合は、その弱点を回避する形で :is() / :where() による詳細度設計に行き着きました。

次の記事では、この詳細度設計の土台になっているITCSSとFLOCSSの考え方、そして実際のレイヤー分けの全体像を紹介します。また、:is() / :where() をはじめとした擬似クラス関数ごとの詳細度の挙動については、図解付きの記事で詳しく整理する予定です。


関連記事

CSS設計|ITCSS×FLOCSSのハイブリッド構成と詳細度設計の実例 ITCSSとFLOCSSはどちらもCSS設計の代表的な手法ですが、性質も得意分野も異なります。両者の違いを整理しつつ、色トークン系ファイルの置き場に迷った実例や、`:is()` / `:where()` を使って詳細度を固定する6層構成のハイブリッド設計を紹介...  続きを読む CSS詳細度を図解する:IDs・class・typeから :is() :where()まで CSSが「思った通りに上書きされない」とき、原因の多くは詳細度(specificity)の理解不足です。前回までの記事で :is() / :where() を使った詳細度設計の話をしてきましたが、そもそも詳細度がどう計算されるのかを整理していなかったので、今回...  続きを読む CSSで「何番目か」を指定する:nth系セレクタとクラスベース位置選択の完全ガイド :nth-of-type・:nth-last-of-type の式の読み方から、:is() / :not() / :has() を使ったクラスベース位置選択まで網羅。「最初と最後以外」の複数の書き方と詳細度の比較も収録。CSS設計の実務に役立つリファレンス  続きを読む