CSSの@layerで、スタイルの優先順位を整理する方法

​​CSS​Playwright

CSS の @layer(カスケードレイヤー)を使うと、スタイルの勝ち負けを「どのレイヤーに書いたか」で決められます。後のレイヤーに書いたスタイルは、前のレイヤーのスタイルより詳細度が低くても勝ちます。ID セレクターで強く書かれたスタイルを上書きするために、セレクターを長くしたり !important を足したりする必要がなくなります。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。確かめたい書き方ごとに検証用のページを作り、Chromium で開いて、要素の文字色を getComputedStyle() で調べました。ブラウザが CSS をどう読み取ったかは、document.styleSheets のルールの一覧(CSSOM)で確かめています。Firefox と Safari は試していません。

先頭でレイヤーの順番を宣言し、ルールを振り分ける

まず、使うレイヤーの名前を、弱いものから順にカンマで並べて宣言します。そのあと、@layer 名前 { … } のブロックに、それぞれのルールを書きます。

css
@layer base, utilities;

@layer base {
  #t.box { color: red; }
}
@layer utilities {
  .text-blue { color: blue; }
}
html
<p id="t" class="box text-blue">文字</p>

#t.box は ID とクラスを含むので、.text-blue よりずっと詳細度が高いセレクターです。それでも、検証環境で試したところ、文字は青になりました。後に宣言した utilities のスタイルが勝っています。

MDN の @layer のページにあるとおり、レイヤーの順番で勝ち負けが決まったあとは、詳細度も、ルールを書いた位置も関係しません。詳細度で勝ち負けが決まるのは、同じレイヤーの中で競合したときだけです。

次の画像は、ボタンの背景色で同じことを試したものです。@layer を使わないと、詳細度の高い #send.btn が勝ちます。レイヤーに分けると、後のレイヤーにある .bg-pink が勝ちます。

2つの枠を縦に並べた画面。上の枠は「@layer を使わない」で、#send.btn に灰色、.bg-pink にピンクの背景を指定したコード。その下の「送信する」ボタンは灰色。下の枠は「@layer で順番を決める」で、同じ2つのルールを base と utilities のレイヤーに分けたコード。その下の「送信する」ボタンはピンク
同じ2つのルールでも、レイヤーに分けると勝ち負けが変わる(Chromium 153)

順番は、そのレイヤーの名前が最初に出たところで決まる

レイヤーの順番は、名前が CSS の中で初めて出てきた順です。先頭で順番を宣言しないと、@layer のブロックを書いた順がそのままレイヤーの順番になります。

css
@layer components { .t { color: blue; } }
@layer base { .t { color: red; } }

このように書くと、先に現れた components が弱いレイヤーになります。検証環境では、文字は base の赤になりました。同じ2つのブロックの前に @layer base, components; を1行足すと、文字は components の青に変わりました。

一度決まった順番は、あとから変えられません。先ほどの CSS の最後に @layer components, base; と逆の順番を書き足しても、検証環境では文字は青のままでした。CSSOM を見ると、後ろの宣言もルールとしては残っています。ただし、すでにあるレイヤーの順番には影響していません。

text
CSSLayerStatementRule nameList=[base, components]
CSSLayerBlockRule name="components"
  CSSStyleRule ".t" { color: rgb(0, 0, 255); }
CSSLayerBlockRule name="base"
  CSSStyleRule ".t" { color: rgb(255, 0, 0); }
CSSLayerStatementRule nameList=[components, base]

スタイルシートを分けている場合も同じです。検証環境で、1つ目の <style> に @layer base, components; だけを書き、2つ目の <style> に上の components と base のブロックを書いたところ、文字は components の青になりました。1つ目で宣言した順番が、2つ目のスタイルシートにも効いています。MDN の学習ページ「カスケードレイヤー」でも、順番は前もって宣言しておくのがよいとしています。最初に読み込む CSS の先頭に、@layer reset, base, components, utilities; のような1行を置いておくと、順番を1か所で管理できます。

レイヤーの外に書いたスタイルは、どのレイヤーよりも強い

@layer の中に入れなかったスタイルは、すべてのレイヤーより強くなります。

css
p { color: green; }

@layer utilities {
  #t.text-blue { color: blue; }
}

検証環境では、文字は緑になりました。レイヤーの外の p は、この例でいちばん詳細度の低いセレクターで、しかもレイヤーより前に書いてあります。それでも、レイヤーの中の #t.text-blue に勝っています。名前のないレイヤー(@layer { … })に書いたスタイルも同じで、レイヤーの外のスタイルの後に書いても、レイヤーの外のスタイルに負けました。

CSS Cascading and Inheritance Level 5 の仕様には、レイヤーの外のスタイルは、最後にある見えないレイヤーに入っているものとして扱うと書かれています。

これは、既存の CSS に少しずつ @layer を入れていくときに効いてきます。新しく書いたスタイルだけをレイヤーに入れると、レイヤーの外に残っている古いスタイルのほうが勝ちます。後から足したスタイルを効かせたいなら、古いスタイルを弱いレイヤー(legacy など)に入れていきます。

外部の CSS は @import でレイヤーに入れる

ライブラリーなど、自分では中身を書き換えない CSS は、@import に layer() を付けて読み込むと、レイヤーに入れられます。

css
@import url("vendor.css") layer(vendor);
@layer vendor, components;

@layer components {
  .t { color: blue; }
}

検証では、vendor.css に #t-import { color: red; } という ID セレクターのルールを書いておきました。layer(vendor) を付けて読み込むと、文字は components の青になりました。layer() を付けずに読み込むと、vendor.css のルールがレイヤーの外に入り、文字は赤になりました。

MDN の @import のページにあるとおり、@import は、@charset とレイヤーの順番の宣言(@layer a, b; の形)を除いて、ほかのすべてのルールより前に書きます。検証環境で、@layer components { … } のブロックの後ろに @import を書いたところ、@import は無視されました。CSSOM にも @import のルールが残っていませんでした。

<link> 要素で読み込む CSS をレイヤーに入れる書き方はありません。MDN の学習ページ「カスケードレイヤー」でも、その場合は @import を使うよう案内しています。

!important を付けると、レイヤーの順番が逆になる

!important を付けた宣言どうしでは、強さの順番がひっくり返ります。

css
@layer base, utilities;

@layer base {
  .t { color: red !important; }
}
@layer utilities {
  .t { color: blue !important; }
}

検証環境では、文字は赤になりました。!important どうしでは、先に宣言した base が勝ちます。レイヤーの外とレイヤーの中の関係も逆になり、どちらにも !important を付けて、レイヤーの外の .t(緑)とレイヤーの中の .t(赤)を比べると、赤が勝ちました。!important を外すと、レイヤーの外の緑が勝ちます。

@layer base, components, utilities; のときの強さを表した図。左の「通常の宣言」の列は、上から base、components、utilities、レイヤーの外の順に並び、いちばん下のレイヤーの外がピンク。右の「!important の宣言」の列は、上からレイヤーの外、utilities、components、base の順に並び、いちばん下の base がピンク。上ほど弱く、下ほど強い
通常の宣言と !important の宣言で、レイヤーの強さの順番が逆になる

MDN の @layer のページにあるとおり、!important を付けた宣言は、付けていない宣言よりも優先されます。そのうえで、!important を付けた宣言どうしの順番が、通常の順番の逆になります。

この仕組みを使うと、弱いレイヤーに「上書きさせたくない」指定を置けます。たとえば reset や base のレイヤーで !important を付けると、後ろのレイヤーで !important を付けても上書きできません。

入れ子のレイヤーは、親のレイヤーの中で順番が決まる

レイヤーの中に、さらにレイヤーを作れます。親のレイヤーの中にある子のレイヤーに、ブロックの外からルールを足すときは、framework.layout のように名前を . でつなぎます(MDN の @layer のページ)。

css
@layer framework {
  @layer layout, theme;
}
@layer framework.theme  { .t { color: blue; } }
@layer framework.layout { .t { color: red; } }

検証環境では、文字は青になりました。親の中で layout, theme の順に宣言したので、後に書いた framework.layout より framework.theme が強くなっています。

親のレイヤーに直接書いたスタイルは、その中の子のレイヤーより強くなりました。@layer framework { … } の中に .t { color: red; } と、子のレイヤー layout に入れた .t { color: blue; } を書くと、文字は赤でした。外側の関係と同じで、子のレイヤーに入っていないスタイルのほうが勝ちます。

子のレイヤーは、親のレイヤーの順番の外には出ません。@layer framework, app; と宣言したうえで、framework.layout に詳細度の高い #t.t { color: red; } を書いても、app の .t { color: blue; } が勝ち、文字は青になりました。ライブラリーをまるごと1つのレイヤーに入れておけば、通常の宣言どうしでは、その中でどんなレイヤーが使われていても、自分のレイヤーより弱いままになります。

下のレイヤーの値に戻すには revert-layer を使う

レイヤーの中で、ある要素だけそのレイヤーの指定を取り消したいときは、値に revert-layer を書きます。

css
@layer base, components;

@layer base {
  .t { color: green; }
}
@layer components {
  .t { color: red; }
  .t.plain { color: revert-layer; }
}
html
<p id="t" class="t plain">文字</p>

検証環境では、components の赤ではなく、base の緑になりました。MDN の revert-layer のページによると、revert-layer は、そのレイヤーで要素にルールを指定していなかったものとして値を計算し直します。下のレイヤーにも指定がなければ、ブラウザの既定のスタイルなどに戻ります。

対応しているブラウザ

MDN のブラウザ互換性データによると、@layer に対応したのは Chrome 99、Firefox 97、Safari 15.4 です。revert-layer の互換性データでも、同じバージョンから対応しています。

​Posted by 86