このシリーズでは、CSS の重要な概念であるカスケード(Cascade)の基礎を改めて学び直し、その応用としてカスケードレイヤー(@layer
)について説明していきます。
前回の基礎編では CSS カスケードの全体像を説明しましたが、今回はそれらの知識を踏まえてカスケードレイヤーの仕組みや revert-layer
キーワードの使い方について詳しく見ていきます。
詳細度合戦
見出し「詳細度合戦」CSS のカスケード(Cascade)は、同一要素に対してスタイルが競合しているときに、優先順位を決めるルールです。
ある要素に指定されたスタイルを上書きしたいときに詳細度を上げることで実現できますが、この詳細度合戦がエスカレートしていくと !important
フラグにたどりつきます。これは最終手段であり、確信を持って使用するとき以外は避けるべきです。
この争いを避けるために、原則クラスセレクタのみを使用するといったような、できるだけ詳細度を低く維持する工夫がされてきました。
しかし、詳細度が同じ場合には後に出現するスタイルが優先されるため、CSS のコードが長くなるほど優先順位の管理が難しくなる課題が依然として残ります。
加えて、サードパーティ製の CSS や、ブログ記事の Markdown のように class
を付与することが難しいケースでは、思わぬところで詳細度の取り扱いに苦労することがあります。
カスケードレイヤー(@layer
)を使用することで、設計レベルで優先順位を制御することができ、これらの課題を解決できます。
カスケードレイヤーの作成方法
見出し「カスケードレイヤーの作成方法」カスケードレイヤー(@layer
)の仕組み自体は、前回の記事で説明した CSS カスケードの内容が理解できていればそれほど複雑ではありません。
先ほどの CSS コードに対して、カスケードレイヤーを使用して優先順位を整理すると以下のようになります(HTML は同じです)。
レイヤーを使用して優先順位を定義することによって、先ほどのコードで .alert
のスタイル指定に付与していた !important
は不要になりました。
カスケードレイヤーを作成する方法は、おおまかに以下の 3 パターンに分けられます。
@layer
ブロックを使用する@layer
宣言文を使用する@import
規則で使用する
順番に見ていきましょう。
@layer
ブロックを使用する
この見出しのリンクまずは、ルールセットを @layer
ブロックで囲う方法です。
この例では、reset
と components
という名前のレイヤーを作成しています。
ここで前回のおさらいをすると、カスケードレイヤーでは、後に作成したレイヤーのほうが優先されます。さらに、@layer
を使用しない従来のルールセット(Un-Layered)があれば、このレイヤーを使用しない Un-Layered なスタイルのほうが優先されます。
この例で、詳細度だけに着目すれば「p#foo
> p.bar
> p
」ですが、レイヤーは詳細度よりも前の段階で評価されるため、優先順位は「Un-Layered > second
> first
」です。
- Un-Layered
- second
- first
そして、このレイヤーの順番は !imortant
フラグを使用すると反転します。
- first
!important
- second
!important
- Un-Layered
!important
- Un-Layered
- second
- first
レイヤーは作成した時点で順位が確立されるため、後から同じ名前のレイヤーを指定しても優先順位が入れ替わることはありません。
この例では、first
、second
の順にレイヤーを定義して、再び first
レイヤーのブロックを記述していますが、すでにレイヤーの順位は確立しているので入れ替わることはなく、second
レイヤーの color: green
が適用されます。
@layer
ブロックのみでレイヤーを作成する方法では、このようにコードの出現順によって優先順位が確立してしまうため、ソースオーダーに依存してしまう課題が残ります。
この課題を解消するために、続いて @layer
宣言文の指定を見ていきましょう。
@layer
宣言文を使用する
この見出しのリンク@layer
宣言文は、明示的にレイヤーの優先順位を指定する方法です。
@layer
ブロックと同様に、後に指定したレイヤーが優先されるので、このコードの優先順位は以下のとおりです。
- components
- layouts
- defaults
- reset
通常はこの宣言の後に、@layer
ブロックでスタイルを指定します。
@layer
宣言文でも、ブロックを使用したときと同様に、一度確立した優先順位を後から入れ替えることはできません。例を見てみましょう。
2 回目の宣言で components
と layouts
が再び定義されていますが、すでに確立しているため順位は変わらず、新たに宣言された utilities
レイヤーのみが追加されます。
このコードの最終的な優先順位は以下のとおりです。
- utilities
- components
- layouts
- defaults
- reset
このように、複数箇所で宣言するとレイヤーの優先順位がわかりづらくなるので、@layer
宣言文はソースコードのなるべく先頭に一貫性を持たせて管理するのがよいでしょう。
なお、@layer
ブロックの後に @layer
宣言文を指定したり、@media
規則の条件分岐を利用して順位を変えることもできますが、予測がしづらく複雑になるだけなので避けるべきです。
デベロッパーツールでレイヤーを確認する
見出し「デベロッパーツールでレイヤーを確認する」Google Chrome や Microsoft Edge のデベロッパーツールでは、@layer
の対象となる要素を選択すると、「Elements」タブの「Styles」に「Toggle CSS Layers view」のボタンが出現します。
このトグルボタンを選択すると、レイヤーの構造や優先順位を確認できます。
@import
規則で使用する
この見出しのリンク最後に @import
規則で読み込んだ CSS にレイヤーを割り当てる方法です。
このコードでは、@import
規則の URL 指定の直後の layer()
関数によってインポートした foo.css
を libs
レイヤーに割り当てています。
レイヤーの優先順位は以下のとおりです。
- components
- libs
- layouts
- defaults
- reset
これによって、外部ライブラリやフレームワークなどの、サードパーティ製の CSS に書かれているセレクタの詳細度に関わらず、スタイルの優先順位を明示することができます。
ただ、「Optimize resource loading | web.dev」にあるように、@import
規則を使用した場合、宣言されている CSS のダウンロードが完了するまで読み込みが開始せず、Preload Scanner も有効にならずにパフォーマンスに悪影響を及ぼすので、やむを得ない場合を除いては使用を控えたほうがよいでしょう。
なお、<link>
要素に対してレイヤーを指定する機能の追加については「Allow authors to apply new css features (like cascade layers) while linking stylesheets | Issue #7540 | whatwg/html」で議論されています。
@layer
はスタッキングコンテキストには影響しない
この見出しのリンク「レイヤー」という名称から誤解しやすいのですが、カスケードレイヤーの指定はスタッキングコンテキスト(要素の重なり順)には影響しません。
以下の例では、top
レイヤーの優先順位を高くしていますが、重なり順には影響しません。結果的には HTML コードが後方にある .bottom
が上に重なります。
<!-- HTML -->
<div class="top">Top</div>
<div class="bottom">Bottom</div>
<!-- CSS -->
<style>
@layer base, bottom, top;
@layer base {
.bottom, .top {
inline-size: 10em;
aspect-ratio: 1;
color: white;
}
}
@layer bottom {
.bottom {
position: absolute;
inset-block-start: 40px;
inset-inline-start: 40px;
background-color: blue;
}
}
@layer top {
.top {
position: absolute;
background-color: green;
}
}
</style>
匿名レイヤー
見出し「匿名レイヤー」ここまで 3 パターンのレイヤー定義方法を見てきましたが、レイヤーは名前を指定しなくても作成できます。これらのレイヤーは「Anonymous Layer(匿名レイヤー)」や「Un-named Layer(無名レイヤー)」と呼ばれます。
用語が確立していないので迷いますが、W3C や MDN のドキュメントでは「Anonymous Layer」のほうが多いようなので、本記事では「匿名レイヤー」で統一します。
匿名レイヤーは、以下のように @import
規則、もしくは @layer
ブロックで作成できます。
@import
規則の場合には url()
の後に layer
キーワードを付与し、@layer
ブロックの場合には名前を付けずに指定するだけです。
無名レイヤーは、ソースコードで出現した場所で順位が確立されるため、@layer
宣言文でレイヤーの優先順位を定義することができません。
通常のレイヤーの管理では使用するケースは少ないと思われますが、意図的にレイヤーの外側からアクセスできないようにしたり、一時的に優先度を調整するためのテクニックとして使用することができます。
後述する revert-layer
を使用したカプセル化の例で、このテクニックを使用しています。
レイヤーのネスト
見出し「レイヤーのネスト」カスケードレイヤーはネストしてサブレイヤーを作成することができます。
レイヤーのネストは、ピリオド(.
)で連結して表現できます。上記のコードを書き換えると次のようになります。
上記のコードの優先順位は以下のようになります。
- themes
- themes.dark
- themes.light
- components
- components.button
また、レイヤー名が同じでも階層が異なれば別のレイヤーとして扱われます。
@layer
宣言文を使用して、ネストしたサブレイヤーの順位を定義することも可能です。
2 行目の宣言文によって、themes.dark
より themes.light
のほうが優先度が高くなり、先ほどの順位は以下のように変わります。
- themes
- themes.light
- themes.dark
- components
- components.button
ここで改めて注目しておきたい点は、ネストの内側のレイヤーよりも、外側のレイヤーのほうが優先度が高いということです(themes.light < themes
)。
上記のコードでは、宣言した順番だけを見れば themes.dark
や themes.light
のほうが後ですが、サブレイヤーが親レイヤーの優先度を超えません。
このことは、@layer
を使用しない Un-Layered の指定は @layer
を使用した指定よりも優先度が高いというルールと同じです。つまり、原則はレイヤーの外側のほうが優先度が高くなります。
そして、これまでと同様に !important
フラグを使用すると順位は入れ替わるので、例外的にサブレイヤーの優先度を親レイヤーよりも高くすることができます。
- components.button
!important
- components
!important
- themes.dark
!important
- themes.light
!important
- themes
!important
- themes
- themes.light
- themes.dark
- components
- components.button
revert-layer
見出し「revert-layer」前回の基礎編では、オリジンの段階をロールバックする revert
キーワードについて説明しましたが、revert-layer
キーワードはそのカスケードレイヤー版です。
revert-layer
を指定することで、レイヤーをロールバックすることができます。
上記のコードでは、layouts
レイヤーで aside p
に対して color: revert-layer
を指定しています。これによって defaults
レイヤーまで巻き戻り、color: green
が適用されます。
- components
-
layouts
revert-layer
- defaults
- reset
この仕組みはレイヤーをネストしている場合も同様です。以下は themes.light
レイヤーに対して revert-layer
キーワードを指定したイメージです。
- themes
- themes.dark
-
themes.light
revert-layer
- components
- components.button
同じ要素に対する競合するスタイルが見つかるまでロールバックしていき、レイヤーに見つからない場合には最終的にはオリジンまで戻ります。
つまり、revert
を指定したときのように、User オリジン、User-Agent オリジンとたどっていき、それでも見つからなければ unset
を指定したのと同様に扱われます。
revert-layer
に !important
フラグを指定する
この見出しのリンクさて、以下のように revert-layer
キーワードを指定した宣言に、!important
フラグを指定するとどうなるでしょうか。
@layer components, themes;
@layer components.button;
@layer themes.light, themes.dark;
@layer themes.light {
p {
color: revert-layer !important;
}
}
以下は themes.light
レイヤーに対して、revert-layer
と !important
フラグを指定したときのイメージです。
- components.button
!important
- components
!important
-
themes.light
!important
-
themes.dark
!important
-
themes
!important
- themes
- themes.dark
-
themes.light
revert-layer
- components
- components.button
先ほどと異なり直感的ではないですが、!important
フラグが指定された themes.light
から通常の themes.light
までの範囲がすべて取り除かれます。
そのため、この範囲内で !important
フラグを使用した強いスタイルがあったとしても、指定が存在しないかのように components
レイヤーまでロールバックします。
revert-layer
を使用したカプセル化
この見出しのリンク以下のコードは、Mayank 氏の「Some use cases for revert-layer」というブログ記事で紹介されているテクニックをアレンジしたものですが1、revert-layer
キーワードを使用して外部のスタイルをカプセル化することができるようです。
このコードを見ただけでは、なぜコンポーネントの外側のスタイルを防ぐことができるのかを理解するのは難しいのですが2、順番にその仕組みを確認していきます。
上記のコードに検証用のレイヤーとスタイルを追加します。
<!-- HTML -->
<my-component>
Lorem ipsum
</my-component>
<!-- CSS -->
<style>
@layer components, overrides;
/* 対象となるコンポーネントのレイヤー */
@layer components {
my-component {
/* コンポーネント内でのみ使用するプライベートなレイヤー */
@layer {
color: green;
}
/* 外部のすべてのスタイルを流入させない指定 */
all: revert-layer !important;
}
}
/* 検証用のスタイル */
/* これらのスタイルで上書きされないことを確認する */
my-component {
color: white !important;
background-color: red !important;
text-transform: uppercase;
}
@layer overrides {
my-component {
color: lightcyan;
background-color: teal !important;
text-decoration: line-through;
}
}
</style>
上記のコードの優先順位は以下のとおりです。なお、components.(anonymous)
は匿名レイヤーを指しています。
- components.(anonymous)
!important
- components
!important
- overrides
!important
- overrides
- components
- components.(anonymous)
この状態で components
に revert-layer
キーワードと !important
フラグを指定すると、そこから通常の components
までの範囲が無効化されます。
- components.(anonymous)
!important
-
components
!important
-
overrides
!important
- overrides
-
components
revert-layer
- components.(anonymous)
この動作によって、その下の components.(anonymous)
までロールバックします。
そして、この匿名レイヤーで指定していないプロパティ(color
プロパティ以外)は、all
プロパティの指定によってオリジンまで戻ります。
実際に確認すると、先ほど追加した検証用のスタイルは適用されずに、匿名レイヤーに指定した color: green
のみが有効になることがわかります。
なお、先ほどのコードは <my-component>
要素のみを対象としているので、その子孫要素も対象に含めるには、以下のように全称セレクタ(*
)を使用します。
注意点として、ここで紹介したコードは外部のスタイルの流入は防げますが、内部のスタイルは外部のスタイルへ影響を及ぼす可能性があります。CSS だけでこの課題に対応するには @scope
規則が必要になりますが、現時点では Firefox がサポートしていません。
リセット CSS をピンポイントに適用する
見出し「リセット CSS をピンポイントに適用する」上記の revert-layer
のテクニックを発展させてみましょう。
もし、components.(anonymous)
より低いレベルのレイヤーが存在すれば、オリジンに戻るよりも先にそのレイヤーが評価されます。
例えば、先ほどのレイヤー構成の最下層に reset
レイヤーを追加することで、リセット CSS のみをピンポイントに適用することができます。
- reset
!important
- components.(anonymous)
!important
-
components
!important
-
overrides
!important
- overrides
-
components
revert-layer
- components.(anonymous)
- reset
このテクニックによって、コンポーネントの外側のスタイルが流入するのを防ぎつつ、リセット CSS をベースに匿名レイヤーにスタイルを書いていくことが可能になります。
CSS 変数(カスタムプロパティ)は無効化されない
見出し「CSS 変数(カスタムプロパティ)は無効化されない」all
プロパティはすべてのプロパティのショートハンドとして使用されますが、例外的に direction
、unicode-bidi
、そして CSS 変数(カスタムプロパティ)は含まれません。
そのため、この revert-layer
のテクニックを用いても、CSS 変数は無効化されずに適用されます。
以下のコードでは、Un-Layered に指定している border
は無効化されますが、CSS 変数(--color
)は有効なままなので、color: blue
が適用されます。
CSS 変数もカプセル化するには、個別に revert-layer
を指定します。
カスケードレイヤーの課題
見出し「カスケードレイヤーの課題」ここまで、カスケードレイヤー(@layer
)の仕組みや使い方について説明しましたが、運用するにあたっての課題を考えてみます。
トップレベルレイヤーの影響度
見出し「トップレベルレイヤーの影響度」@layer
宣言文で作成した後のレイヤーの順番を変更すると広い範囲で影響が及ぶ可能性があります。そのため、特にトップレベルレイヤーは慎重に設計し、作成後はレイヤーの生態系が崩れないように注意する必要があります。
もしくは、優先順位を低くしたいリセットやベースとなるスタイルのレイヤーのみを用意して、コンポーネントのようなウワモノに相当するレイヤーはあえて作成しない(Un-Layered にする)といった方針も考えられます。
Un-Layered の優先度
見出し「Un-Layered の優先度」すべてのスタイルをカスタムレイヤーで管理しているときに、不用意に @layer
ブロックを付け忘れてコードを記述していたとしても、そのスタイル自体は問題なく適用されます。
しかし、そのスタイルは Un-Layered のレベルを持つため、意図しない形で強い優先度を持ってしまいます。
レイヤー名のタイプミスによる影響
見出し「レイヤー名のタイプミスによる影響」@layer
ブロックでレイヤー名を誤ると、優先度の強いスタイルが出来上がってしまいます。
@layer reset, defaults, layouts, components, overrides;
/* components(複数形)を component(単数形)とタイプミス */
@layer component {
button {
/* ... */
}
}
この例では、先頭の @layer
宣言文で優先順位を定義しており、そのあとの @layer
ブロックで components
と複数形で書くところを、誤って component
と単数形で指定したため、overrides
よりも強いレイヤーが作られてしまっています。
先ほどの @layer
ブロックの付け忘れと同様に、CSS の構文上は何の問題もないので、ミスに気づきづらいというのも厄介なポイントです。
ネストが深くなる
見出し「ネストが深くなる」従来の @media
規則や @container
規則、今後サポートが広がると考えられる @scope
規則、そして CSS のネスト記法(CSS Nesting)、そしてさらに @layer
ブロックが加わると、ネストが深くなりコードの見通しが悪くなる可能性があります。
@layer components {
@layer button {
@container (min-inline-size: 600px) {
@scope (.foo) to (.bar) {
button {
svg {
/* 😵💫 */
}
}
}
}
}
}
Progressive Enhancement な実装ができない
見出し「Progressive Enhancement な実装ができない」カスケードレイヤー(@layer
)は 2022 年より主要なブラウザでサポートされており、Safari においては 15.4 からサポートしています。
そのため、多くの環境で十分にサポートされていると考えられますが、カスケードレイヤーは CSS 全体の優先順位に関わるので、非対応の環境では表示が大幅に崩れる可能性があります。
PostCSS の polyfill はありますが、Progressive Enhancement な実装はできない点には留意しておいたほうがよいでしょう3。
おわりに
見出し「おわりに」前回の CSS カスケードの全体像の説明に続き、今回はカスケードレイヤー(@layer
)の仕組みや revert-layer
キーワードの使い方を説明しました。
基本的なカスケードレイヤーの仕組み自体はわりと理解しやすいのですが、実際にどのようなレイヤー構成にすればよいのか、運用時にどのような問題が発生しうるのかといったポイントは、引き続き実践をとおして考えていかなければなりません。
また、revert-layer
を使用したカプセル化のテクニックのような、カスケードレイヤーの新たな可能性も探究していきたいです。
参考文献
見出し「参考文献」本記事の作成にあたり、以下のウェブページを参考にしました。
- Cascade layers | MDN(外部リンクを開く)
- CSS Cascading and Inheritance Level 5 | W3C(外部リンクを開く)
- A Complete Guide to CSS Cascade Layers | CSS-Tricks(外部リンクを開く)
- The Future of CSS: Cascade Layers (CSS @layer) | Bram.us(外部リンクを開く)
- Getting Started With CSS Cascade Layers | Smathing Magazine(外部リンクを開く)
- Some use cases for revert-layer | Mayank(外部リンクを開く)
脚注
-
さらに元となるアイデアは Nathan Knowler 氏の「So, You Want to Encapsulate Your Styles?」というブログ記事です。 ↩
-
ちなみに、本シリーズを執筆するきっかけはこのコードの仕組みを理解するためでした。 ↩
-
この表現は Chris Coyier 氏のブログ記事「What You Need to Know about Modern CSS (Spring 2024 Edition) | Frontend Masters Boost」を参考にしました。 ↩