そろそろカスケードレイヤー(@layer)と向き合う
カスケードレイヤー@layer
がモダンブラウザでサポートされたとはいえ、互換性の問題から、実際に現場で見かけるのは、まだまだ未来の話と思っていました。ふと、思い立ってCan I use... Support tables for HTML5, CSS3, etc
を確認してみたところ、現時点(2022.12.27)でも、すでにGlobalで9割近くサポートされています。これは来年あたりから見かける機会が増えそうだと思ったので、そろそろカスケードレイヤーと向き合いたいと思います。
@layer
カスケードレイヤーカスケードレイヤーはスタイルの優先順位を制御するためアルゴリズムの1つです。カスケードレイヤーを使用することで、スタイルの優先順位をレイヤー化(階層化)して管理することができます。とりわけ覚えておかなければならないのは、カスケードアルゴリズムは詳細度アルゴリズムよりも優先されるということです。つまり、詳細度で負けているスタイルでも、カスケードレイヤーによっては、優先されて適用される可能性があります。
言葉だけでは分かりづらいので、単純な例をみてみましょう。
<p id="hoge" class="hoge">piyo</p>
/* 詳細度の比較で`#hoge { color: red }`が優先される */
#hoge { color: red }
.hoge { color: blue }
上記のようなHTMLとCSSの記述があるとします。詳細度の比較において、IDセレクターはクラスセレクターよりも優先されるため、上記の記述ではcolor: red
が適用されます。
次にカスケードレイヤーを導入した例をみてみます。
/*
最初にレイヤーの順序を宣言する
下記の場合overwriteが優先される
*/
@layer base, overwrite;
@layer base {
#hoge { color: red }
}
@layer overwrite {
.hoge { color: blue }
}
@layer
を使用して、レイヤーの宣言をしています。カスケードレイヤーのアルゴリズムは、詳細度よりも優先されるため、上記の場合はoverwrite
レイヤーのcolor: blue
が適用されます。
このようにカスケードレイヤーを導入することで、従来の詳細度に頼ったCSS設計とは別の軸で、スタイルの優先順位を管理できるようになります。
基本構文
カスケードレイヤーには、次の3つの宣言方法があります。
- ブロック構文と
@layer
による宣言 - 一文構文と
@layer
による宣言 -
layer関数
もしくはlayerキーワード
を伴う@import
による宣言
順に確認していきましょう。
@layer
による宣言
ブロック構文とまずはブロック構文と@layer
による宣言について確認してみます。
@layer レイヤーの名前 {
/* スタイルの記述 */
}
@layer
でレイヤーの名前を宣言して、ブロック{}
の中にスタイルを記述します。次の例では詳細度に関係なく、後に書いたレイヤーが優先されて適用されます(レイヤーの順序を参照)。
<p class="hoge">fuga</p>
/* 後に記述した`overwrite`が優先される */
@layer base {
.hoge { color: red; }
}
@layer overwrite {
p { color: blue }
}
またレイヤー名を伴わずに、匿名レイヤーを宣言することができます。
@layer {
.hoge { color: red }
}
@layer
による宣言
一文構文と次に、一文構文について確認してみます。
@layer レイヤーの名前その1, レイヤーの名前その2;
一文構文では、スタイルの伴わないレイヤーを宣言することができます。こちらはブロック構文とは異なり、レイヤーの優先順位のみに関与します(レイヤーの順序を参照)。
/* 一文構文により`overwrite`が優先される */
@layer base, overwrite;
@layer overwrite {
p { color: blue }
}
@layer base {
.hoge { color: red }
}
layer関数
もしくはlayerキーワード
を伴う@import
による宣言
@import
でインポートしたCSSに対して、レイヤーを宣言することもできます。この場合は、@layer
ではなく、layer関数
か、もしくはlayerキーワード
を使用します。
@import url("**/*.css") layer(レイヤーの名前);
上記のようにlayer関数
を使用する場合は、引数に宣言するレイヤーの名前を渡すことができます。
/* `baseレイヤー`と`customレイヤー`が宣言される */
@import url("base.css") layer(base);
@import url("custom.css") layer(custom);
一方で、次のようにlayerキーワード
で宣言した場合、それは匿名レイヤーとして宣言されることになります。
/* これは匿名レイヤーとなる */
@import url("**/*.css") layer;
レイヤーの順序
レイヤーの宣言方法について理解したところで、次はレイヤーの順序について確認してみましょう。カスケードレイヤーでは、各レイヤーが最初に宣言された順序でソートされます。このとき、後にくるレイヤーの優先順位が高くなります。
.hoge { color: red }
@layer reset {
.hoge { color: green }
}
@layer base {
.hoge { color: blue }
}
@layer overwrite {
.hoge { color: yellow }
}
たとえば上記の場合、各レイヤーは次の順序でソートされます。
-
reset
レイヤー -
base
レイヤー -
overwrite
レイヤー - (暗黙的な外縁レイヤー)
後にくるレイヤーの優先順位が高くなるので、この中で最も優先順位の高いレイヤーは(暗黙的な外縁レイヤー)
であり、最も優先順位の低いレイヤーはreset
となります。
ここで、暗黙的な外縁レイヤーという言葉が登場しました。暗黙的な外縁レイヤーとは、@layer
、layer関数
、layerキーワード
を使用して明示的に宣言されたレイヤーの、外にあるスタイルのことです。上記の例でいう.hoge { color: red }
の部分のことであり、レイヤー化されていない(unlayered)スタイルともいえます。
この暗黙的な外縁レイヤーは、レイヤーの順序において、(暗黙的に)最後に追加されるため、明示的なレイヤーよりも優先順位が高くなります。上記のコードではcolor: red
が適用されることに注意してください。
各レイヤーが最初に宣言された順序でソートされるという事実は、一文構文でも変わりません。たとえば次のコードは、一文構文によって優先順位が決定されています。
/* 一文構文で先に優先順位が決まっている */
@layer overwrite, base, reset;
.hoge { color: red }
@layer reset {
.hoge { color: green }
}
@layer base {
.hoge { color: blue }
}
@layer overwrite {
.hoge { color: yellow }
}
上記においてレイヤーの順序は次のようにソートされます。
-
overwrite
レイヤー -
base
レイヤー -
reset
レイヤー - (暗黙的な外縁レイヤー)
(暗黙的な外縁レイヤー)
の優先順位は、変わらず最も高いことに気を付けてください。reset
、base
、overwrite
の順序は一文構文で簡単に並び替えることができます。
レイヤーの入れ子(ネスト)
レイヤーは入れ子にすることができます。
@layer framework {
.hoge { color: red }
@layer base {
.hoge { color: green }
}
@layer custom {
.hoge { color: blue }
}
}
レイヤーを入れ子にした際、レイヤーのネストが深くなれば深くなるほど、優先順位は低くなります。上記の例において、レイヤーの順序は次のようにソートされます(後にくるレイヤーの方が優先順位が高いことに注意してください)。
-
framework.base
レイヤー -
framework.custom
レイヤー -
framework
レイヤー - (暗黙的な外縁レイヤー)
今回の場合、(暗黙的な外縁レイヤー)
のスタイルが存在しないので、framework
レイヤーである.hoge { color: red }
が優先されることになります。
また、レイヤーの入れ子は、次の例のようにビリオド(.
)区切りで宣言することもできます。
@layer framework.base {
.hoge { color: green }
}
@layer framework.custom {
.hoge { color: blue }
}
@layer framework {
.hoge { color: red }
}
/* 以下と同等になる
@layer framework {
.hoge { color: red }
@layer base {
.hoge { color: green }
}
@layer custom {
.hoge { color: blue }
}
}
*/
既存のレイヤーにスタイルを追加する
次のように同じ名前のレイヤーを複数宣言すると、後に書いたレイヤーのスタイルは既存のレイヤーに追加されます。次の例ではcolor: red
が適用されます。
<p class="hoge">fuga</p>
@layer base {
.hoge { color: red }
}
@layer base {
p { color: blue }
}
/* 以下と同等なので、`color:red`が優先される
@layer base {
.hoge { color: red }
p { color: blue }
}
*/
各レイヤーは最初に宣言された順序でソートされるので、次の例では、overwrite
レイヤーがbase
レイヤーよりも優先されて適用されます。
<p id="hoge" class="fuga">piyo</p>
@layer base {
p { color: green }
}
@layer overwrite {
.fuga { color: red }
}
@layer base {
#hoge { color: blue }
}
/* 以下と同等なので、overwriteレイヤーの`color: red`が優先される
@layer base {
p { color: green }
#hoge { color: blue }
}
@layer overwrite {
.fuga { color: red }
}
*/
匿名レイヤー
本記事において、すでに何度か登場していますが、カスケードレイヤーでは匿名レイヤーを宣言することができます。匿名レイヤーとはレイヤー名を指定しない明示的なレイヤーのことで、暗黙的な外縁レイヤーとは別の存在であることに気をつけてください。
/* @importによる匿名レイヤーの宣言 */
@import url("base.css") layer;
/* ブロック構文による匿名レイヤーの宣言 */
@layer {
.hoge {
color: red;
}
}
匿名レイヤーは宣言ごとに個別のレイヤーを作成する
匿名レイヤーは、宣言ごとに個別のレイヤーを作成します。次のコードではbase
レイヤーを2つのブロック構文に分けて記述しています。この場合、2つ目のブロック構文のスタイルは、既存のレイヤーに追加されます(既存のレイヤーにスタイルを追加するを参照)。
@layer base {
.hoge { color: red }
}
@layer base {
p { color: blue }
}
/* 以下と同等
@layer base {
.hoge { color: red }
p { color: blue }
}
*/
しかし、匿名レイヤーの場合は、個々の独立したレイヤーとして見なされます。
/* 別々のレイヤーと見なされる */
@layer {
.hoge { color: red }
}
@layer {
p { color: blue }
}
/* 以下と同等ではない!!
@layer {
.hoge { color: red }
p { color: blue }
}
*/
次のような記述において、名前があるレイヤーと匿名レイヤーで、挙動が変わることがあるので注意しましょう。
/* これは`color:blue`が優先される */
@layer base {
.hoge { color: red }
}
@layer custom {
.hoge { color: blue }
}
@layer base {
.hoge { color: green }
}
/* これは`color:green`が優先される */
@layer {
.hoge { color: red }
}
@layer custom {
.hoge { color: blue }
}
@layer {
.hoge { color: green }
}
匿名レイヤーは外部から参照できない
匿名レイヤーはそのレイヤーに対して、外部からスタイルを上書きする、もしくは追加する手段を提供しません。あくまで一例ですが、これは以下のようなケースで効果を発揮することがあります。
- チーム開発におけるコード規約を強制する手法として、特定のレイヤーのスタイルを一箇所に限定したい時
- ライブラリとして提供するCSSを外部から操作されないようにしたい時
/*
external.css
いくつかのCSSを匿名レイヤーとして宣言する
*/
@import url(external-reset.css) layer;
@import url(external-base.css) layer;
@import url(external-utility.css) layer;
/*
別のCSSから`external.css`をimportする
内部の匿名レイヤーに対して変更を加えることはできない
*/
@import url(external.css) layer(external);
@layer external {
/* ... */
}
インラインスタイルとの比較
レイヤー化されたスタイルとインラインスタイルとの比較では、インラインスタイルが優先されます。次の例では、color:red
が適用されます。
<!-- `color:red`が適用される -->
<p id="hoge" class="fuga" style="color:red">piyo</p>
@layer {
#hoge.fuga { color: blue }
}
!important
との比較
レイヤー化されたスタイルと!important
を付与されたスタイルとの比較では、!important
を付与されたスタイルが優先されます。
.hoge { color: red }
@layer base {
/* これが優先される */
.hoge { color: green !important }
}
@layer overwirte {
.hoge { color: blue }
}
たとえば上記の例では、各レイヤーは以下の順でソートされます。
-
base
レイヤー -
overwirte
レイヤー - (暗黙的な外縁レイヤー)
ただし、!important
が付与されたスタイルは、カスケードレイヤーよりも優先順位が高いため、上記ではcolor: green !important
が優先されるようになります。
!important
同士の比較はカスケードレイヤーの優先順位を逆転させる
これはうっかりしているとハマりそうな挙動ですが、!important
同士の比較ではカスケードレイヤーの優先順位が逆転します。
実際に例をみて確認してみましょう。ややこしいことに、次の例ではcolor: green !important
が適用されます。
<p class="hoge">fuga</p>
@layer base, overwrite;
.hoge { color: red !important }
@layer base {
/* この記述が適用される!! */
.hoge { color: green !important }
}
@layer overwrite {
.hoge { color: blue !important }
}
何が起きているのか簡単に説明します。まず、!important
のことは一旦忘れて、上記のコードにおけるレイヤーの順序を確認してみます。
-
base
レイヤー -
overwrite
レイヤー - (暗黙的な外縁レイヤー)
ここまでは本記事において何度かやってきたことであり、特に問題はありません。通常では暗黙的な外縁レイヤーが優先されるのですが、!important
同士の比較では、レイヤーの優先順位が逆転します。つまりレイヤーの順序が次のようにソートされます。
- (暗黙的な外縁レイヤー) -
!important
-
overwrite
レイヤー -!important
-
base
レイヤー -!important
したがって、base
レイヤーの!important
の優先順位が最も高くなり、上記ではcolor: green !important
が適用されることとなります。
おわりに
いかがでしたでしょうか。カスケードレイヤーと向き合う話でした。案外ややこしい部分もあったので、導入する際には、事前に知識を整理しておく必要があるように感じました。カスケードレイヤーは便利である一方で、その影響範囲も大きいので、やはりCSS設計をしっかり考えることが重要ですね。
参考
Discussion