こなさんみんばんわ。
意味があるかどうかは別にして、気になったことはとりあえず試してみないと気が済まないタイプの人です。

そんな訳で、そろそろオレも CSS カスケード・レイヤー(以下単にカスケード・レイヤーとします)を導入しようかなと最近ちょっと思ってる訳ですが、何も考えずただ単に全部のスタイルを @layer で囲ってしまうと、非対応ブラウザではまったくスタイルが当たらなくなってしまいます。

なので、せめてそんなブラウザにも最低限のスタイルだけは適用されるようにしたいと思う訳ですが、これが普通に CSS 書くだけではカスケード・レイヤーのその仕様上(あとで説明します)絶対に無理だったりする訳です。

でもそれじゃ困ることもあるので、何とかこの 2 つを両立させる方法はないものか? と色々考えてみた結果、思いついたのはこれくらいしかなかったけど、せっかく考えたのでとりあえずその方法を公開しておくよ…というのが、今回の内容な訳です。訳が多いよ。涙

ざつにカスケード・レイヤーしょうかい

CSS のカスケード・レイヤーというのがどういったものでどのように機能するか、そしてその書き方や具体例は…などといった詳しい解説は、すでに詳しい方たちによる信頼できる情報が Web 上にいくつもアップされておりまして、2025 年にもなって今さらこんな底辺が語る必要性もないのでこの記事でくわしくは書きませんが、ざっくり雑に説明しておくと

  • CSS 仕様に加わった新しい優先順位の概念
  • 従来のセレクタによる詳細度より上位に位置する
    • 詳細度が低くてもレイヤー間の優先順位が高ければスタイルを上書きできる
  • うまく使うことで「スタイルを書いたのに他のルールの方が詳細度高くて変更できない」といった悩みから開放される…はず

といった感じのものです。ところどころ言葉遣いというか微妙な表現が間違ってるかもしれませんがご容赦ください。

実際の CSS においては @layer アットルールを使って、また @import アットルールに layer() 関数や layer キーワードを指定することで、レイヤーの定義やスタイルの追加、優先順位の変更などができます。

/* レイヤーの定義、および優先順位の指定 */
@layer base, layouts, components;

/* @import でレイヤーを指定して外部 CSS をインポート */
@import url(normalize.css) layer(base);

/* レイヤーにスタイルを追加(この時点でレイヤーが未定義の場合は同時に定義される) */
@layer layouts {
  .wrapper {
    max-inline-size: 40rem;
    margin-inline: auto;
  }
}

/* 他にも色々あるけど、あとは仕様などを参照してください… */

カスケード・レイヤー導入で問題になることと、実現したいこと

そんなカスケード・レイヤーですが、導入することで問題になることがあるとすれば、単純にすべてをレイヤーに入れてしまうと非対応の Web ブラウザにはまったくスタイルが適用されなくなることでしょうか。ブラウザは未知のアットルールに遭遇したらその全体を無視することになっているので、@layer で囲まれたスタイル・ブロックも当然ながら、非対応ブラウザではすべて無視されてしまいます。

まぁ現在(2025 年)のブラウザ状況を考えますと、この問題が発生するのは限られたごく少数の環境1のみだとは思いますが、万が一そういった環境からのアクセスがあった場合に見た目が UA スタイルシートのままというのはさすがに残念なので、せめて必要最低限のスタイル2は当たるようにして、別に古い環境を見捨ててる訳じゃないですよ感は出しておきたいなぁ…とか僕は思う訳です。えっあれっみなさんも思いますよね? 思いませんかそうですか。

普通に CSS を書くだけでは上記を実現できない理由

さて、今までの CSS であれば新しい機能やプロパティが実装された際にも、先に古いブラウザ向けのスタイルを書いておき、その後にその新機能やプロパティを用いたスタイルを(必要ならば @supports も併用しつつ)記述することで、先のスタイルを上書きすることや打ち消すことが可能でした。これはまさに CSS が「カスケーディング」スタイルシートであるからこそ…つまりルールの優先順位を決めるアルゴリズムがあることによって実現できていることでもあります。

なので、ついカスケード・レイヤーも同じ感覚で対処できるのではないか、つまり「先にレイヤーを使わない古いブラウザ向けのスタイル、後にレイヤーを使ったスタイルを書くことで、両方で問題なく読み込める CSS が作れるのではないか」と最初は思ってしまう訳ですが、残念ながらこれは叶いません。なぜならカスケード・レイヤーの仕様として、レイヤー化されてないスタイルは、レイヤー内にあるどのスタイルよりも優先して適用されるからです。

See the Pen CSS cascade layer problem by Jeffrey Francesco (@jforg) on CodePen.

このデモは先に挙げた「レイヤー外に古いブラウザ向け、レイヤー内に新しいブラウザ向け」を想定して書いたつもりのものですが、後に書かれたレイヤー内の color: green は適用されず、レイヤー外のスタイルである color: black が適用されているのが分かると思います。:where(p) というセレクタは詳細度が (0, 0, 0) となる一番弱いものですから、詳細度の強弱は関係ないというのも分かりますね。いわば特殊な無名のレイヤーにまとめられて、最後に配置されたような扱いでしょうか。

どうしてカスケード・レイヤーがこのような仕様になっているのかは分かりませんが(おしえてえらいひと)、こういうのは仕様に関わる人たちが何度もディスカッションを重ねた上で、明確な理由をもって決定されることだと思いますので、もし今後のバージョンで何らかの変更が入るとしても、この前提を覆すようなことはしないでしょう。つまり、我々は今後もこの仕様とは付き合っていかなければならないということです。

とにかく、このような仕様上の理由によって、「カスケード・レイヤー非対応ブラウザには最低限のスタイルを適用しつつ、対応ブラウザにはレイヤーをフル活用したスタイルを提供する」という要求は、普通に CSS を書いただけでは絶対に満たせない…というのはお分かりいただけたかと思います。

たどり着いた結論 → JavaScript で機能検出して 2 つの CSS を出し分ける

じゃあどうすればいいのでしょう? 1 つの CSS で実現できないのであれば、あとはもうカスケード・レイヤー対応ブラウザ用と非対応ブラウザ用の 2 つの CSS を作っておいて、ブラウザによって出し分けるしかなさそうです。

その上で、あとはどのように判別をして CSS を出し分けるか…ということになりますが、色々(ない知恵を絞って)考えてみた結果、以下のような方法を思いつきました。というよりこれしか思いつきませんでした。涙

  1. 機能検出用の要素を HTML 上に置く
  2. 適当なレイヤー内に 1. を非表示にするスタイルを書く
  3. JavaScript で 1. の display プロパティ値を取得する
  4. 3. の結果によって非対応ブラウザであればなんかする(なんかって)

デモにしてみると👇こんな感じですかね。まぁ大半のブラウザで赤色のテキストが表示されるだけだとは思いますが😅 編集可能にしておいたので試しに display: block にして Rerun すると変化があると思います。

See the Pen @layer support detection by Jeffrey Francesco (@jforg) on CodePen.

要するにカスケード・レイヤーをサポートしているブラウザでのみ display: none になる要素を配置しておき、その display 値を取得して none になっていなければカスケード・レイヤー非対応と判断して、別のスタイルが当たるようにする…というロジックです。スクリプトも単純な内容なので書くのはそれほど難しくもないですね。

「非対応ブラウザの場合には隠された要素が表示される」という仕組み上、機能検出に使う要素の内容は Web ブラウザのアップデートなどを促すようなものを入れておくのがいいかなーと思います。あとデモではシンプルになるよう単に html 要素に class 名を与えるだけにしてますが、実際には非対応ブラウザ用の CSS を disabled 属性を付けて link しておき、非対応ブラウザであれば disabled を解除するという運用を想定しています。

<head>
  <!-- 省略 -->
  <link rel="stylesheet" href="style.css">
  <link rel="stylesheet" href="legacy.css" disabled>
  <style>@layer hidden { #hidden-message { display: none } }</style>
  <!-- 省略 -->
</head>
<body>
  <div id="hidden-message">
    <!-- カスケード・レイヤー非対応ブラウザ向けのテキスト -->
  </div>
  <script>
    const legacyCss = document.querySelector('link[disabled]')
    const message = document.getElementById('hidden-message')
    const display = getComputedStyle(message).display
    if (display !== 'none') legacyCss.disabled = false
  </script>
  <!-- 以下省略 -->

デモのようなやり方をすれば CSS も 1 つで済みますが、すべてのセレクタを非対応ブラウザ向け class 名と入れ子にしないといけないので面倒ですね。Sass などを使う場合はそれほどでもないでしょうが、ネイティブの CSS Nesting はそもそもカスケード・レイヤーよりサポート・ブラウザのバージョンが新しめですし…(´・_・`)

そんな訳で

本日は CSS カスケード・レイヤーを導入しつつも非対応ブラウザに最低限のスタイルが当たるようにしたいけど、その仕様上の理由でただ CSS を書くだけでは実現不可能なので、JavaScript で機能検出して 2 つの CSS を出し分けるしかなさそう、とりあえずその方法を書いておくよ…というお話でした。

重ね重ねになりますが、現在のブラウザ事情を考えますと、限られたごくわずかのブラウザのためにそこまでやる必要が本当にあるのかってのは微妙なところですが、考えて試した結果機能はしそうだったので、公開だけはしてみた次第です。

まぁそもそも現時点ではまだ僕自身がカスケード・レイヤーを導入してないですし、おそらく Analytics の解析結果などを見て判断することになるかと思いますが、実際に今後使うかどうかも分からんのでね…😭

という訳で、今回の記事は「もしもその必要があるんでしたら、こういう方法が取れますよ」程度に捉えておいていただけますと幸いです。

  1. OS に対する Safari のバージョンが決まっている macOS や iOS などの場合が考えられますが、カスケード・レイヤーに対応した Safari 15.4 以上のバージョンが使えないのは macOS でいうと Catalina (10.15) より前ですからかなり古いですし、iOS も適切にアップデートが適用されている状態であれば、相当古い機種でないと非対応バージョンしか使えないということはないはず。他のブラウザについては自動アップデートですから考えるまでもないでしょう。 

  2. 例えば Web フォントまではロードしないとしてもせめて表示は sans-serif で統一したいとか、大きな画像がブラウザ幅を超えないように max-width で制限を掛けておきたいとか、本当にデフォルトに毛が生えたような最低限のものです。