🙌

そろそろカスケードレイヤー(@layer)と向き合う

2022/12/27に公開約9,100字

カスケードレイヤー@layerがモダンブラウザでサポートされたとはいえ、互換性の問題から、実際に現場で見かけるのは、まだまだ未来の話と思っていました。ふと、思い立ってCan I use... Support tables for HTML5, CSS3, etcを確認してみたところ、現時点(2022.12.27)でも、すでにGlobalで9割近くサポートされています。これは来年あたりから見かける機会が増えそうだと思ったので、そろそろカスケードレイヤーと向き合いたいと思います。

https://caniuse.com/css-cascade-layers

カスケードレイヤー@layer

カスケードレイヤーはスタイルの優先順位を制御するためアルゴリズムの1つです。カスケードレイヤーを使用することで、スタイルの優先順位をレイヤー化(階層化)して管理することができます。とりわけ覚えておかなければならないのは、カスケードアルゴリズムは詳細度アルゴリズムよりも優先されるということです。つまり、詳細度で負けているスタイルでも、カスケードレイヤーによっては、優先されて適用される可能性があります。

言葉だけでは分かりづらいので、単純な例をみてみましょう。

html
<p id="hoge" class="hoge">piyo</p>
css
/* 詳細度の比較で`#hoge { color: red }`が優先される */
#hoge { color: red }
.hoge { color: blue }

上記のようなHTMLとCSSの記述があるとします。詳細度の比較において、IDセレクターはクラスセレクターよりも優先されるため、上記の記述ではcolor: redが適用されます。

次にカスケードレイヤーを導入した例をみてみます。

css
/*
  最初にレイヤーの順序を宣言する
  下記の場合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でレイヤーの名前を宣言して、ブロック{}の中にスタイルを記述します。次の例では詳細度に関係なく、後に書いたレイヤーが優先されて適用されます(レイヤーの順序を参照)。

html
<p class="hoge">fuga</p>
css
/* 後に記述した`overwrite`が優先される */
@layer base {
  .hoge { color: red; }
}
@layer overwrite {
  p { color: blue }
}

またレイヤー名を伴わずに、匿名レイヤーを宣言することができます。

css
@layer {
  .hoge { color: red }
}

一文構文と@layerによる宣言

次に、一文構文について確認してみます。

一文構文
@layer レイヤーの名前その1, レイヤーの名前その2;

一文構文では、スタイルの伴わないレイヤーを宣言することができます。こちらはブロック構文とは異なり、レイヤーの優先順位のみに関与します(レイヤーの順序を参照)。

css
/* 一文構文により`overwrite`が優先される */
@layer base, overwrite;
@layer overwrite {
  p { color: blue }
}
@layer base {
  .hoge { color: red }
}

layer関数もしくはlayerキーワードを伴う@importによる宣言

@importでインポートしたCSSに対して、レイヤーを宣言することもできます。この場合は、@layerではなく、layer関数か、もしくはlayerキーワードを使用します。

@importとlayer関数
@import url("**/*.css") layer(レイヤーの名前);

上記のようにlayer関数を使用する場合は、引数に宣言するレイヤーの名前を渡すことができます。

css
/* `baseレイヤー`と`customレイヤー`が宣言される */
@import url("base.css") layer(base);
@import url("custom.css") layer(custom);

一方で、次のようにlayerキーワードで宣言した場合、それは匿名レイヤーとして宣言されることになります。

@importとlayerキーワード
/* これは匿名レイヤーとなる */
@import url("**/*.css") layer;

レイヤーの順序

レイヤーの宣言方法について理解したところで、次はレイヤーの順序について確認してみましょう。カスケードレイヤーでは、各レイヤーが最初に宣言された順序でソートされます。このとき、後にくるレイヤーの優先順位が高くなります。

css
.hoge { color: red }
@layer reset {
  .hoge { color: green }
}
@layer base {
  .hoge { color: blue }
}
@layer overwrite {
  .hoge { color: yellow }
}

たとえば上記の場合、各レイヤーは次の順序でソートされます。

  1. resetレイヤー
  2. baseレイヤー
  3. overwriteレイヤー
  4. (暗黙的な外縁レイヤー)

後にくるレイヤーの優先順位が高くなるので、この中で最も優先順位の高いレイヤーは(暗黙的な外縁レイヤー)であり、最も優先順位の低いレイヤーはresetとなります。

ここで、暗黙的な外縁レイヤーという言葉が登場しました。暗黙的な外縁レイヤーとは、@layerlayer関数layerキーワードを使用して明示的に宣言されたレイヤーの、外にあるスタイルのことです。上記の例でいう.hoge { color: red }の部分のことであり、レイヤー化されていない(unlayered)スタイルともいえます。

この暗黙的な外縁レイヤーは、レイヤーの順序において、(暗黙的に)最後に追加されるため、明示的なレイヤーよりも優先順位が高くなります。上記のコードではcolor: redが適用されることに注意してください。

各レイヤーが最初に宣言された順序でソートされるという事実は、一文構文でも変わりません。たとえば次のコードは、一文構文によって優先順位が決定されています。

css
/* 一文構文で先に優先順位が決まっている */
@layer overwrite, base, reset;

.hoge { color: red }
@layer reset {
  .hoge { color: green }
}
@layer base {
  .hoge { color: blue }
}
@layer overwrite {
  .hoge { color: yellow }
}

上記においてレイヤーの順序は次のようにソートされます。

  1. overwriteレイヤー
  2. baseレイヤー
  3. resetレイヤー
  4. (暗黙的な外縁レイヤー)

(暗黙的な外縁レイヤー)の優先順位は、変わらず最も高いことに気を付けてください。resetbaseoverwriteの順序は一文構文で簡単に並び替えることができます。

レイヤーの入れ子(ネスト)

レイヤーは入れ子にすることができます。

css
@layer framework {
  .hoge { color: red }
  @layer base {
    .hoge { color: green }
  }
  @layer custom {
    .hoge { color: blue }
  }
}

レイヤーを入れ子にした際、レイヤーのネストが深くなれば深くなるほど、優先順位は低くなります。上記の例において、レイヤーの順序は次のようにソートされます(後にくるレイヤーの方が優先順位が高いことに注意してください)。

  1. framework.baseレイヤー
  2. framework.customレイヤー
  3. frameworkレイヤー
  4. (暗黙的な外縁レイヤー)

今回の場合、(暗黙的な外縁レイヤー)のスタイルが存在しないので、frameworkレイヤーである.hoge { color: red }が優先されることになります。

また、レイヤーの入れ子は、次の例のようにビリオド(.)区切りで宣言することもできます。

css
@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が適用されます。

html
<p class="hoge">fuga</p>
css
@layer base {
  .hoge { color: red }
}
@layer base {
  p { color: blue }
}
/* 以下と同等なので、`color:red`が優先される
@layer base {
  .hoge { color: red }
  p { color: blue }
}
*/

各レイヤーは最初に宣言された順序でソートされるので、次の例では、overwriteレイヤーがbaseレイヤーよりも優先されて適用されます。

html
<p id="hoge" class="fuga">piyo</p>
css
@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 }
}
*/

匿名レイヤー

本記事において、すでに何度か登場していますが、カスケードレイヤーでは匿名レイヤーを宣言することができます。匿名レイヤーとはレイヤー名を指定しない明示的なレイヤーのことで、暗黙的な外縁レイヤーとは別の存在であることに気をつけてください。

css
/* @importによる匿名レイヤーの宣言 */
@import url("base.css") layer;

/* ブロック構文による匿名レイヤーの宣言 */
@layer {
  .hoge {
    color: red;
  }
}

匿名レイヤーは宣言ごとに個別のレイヤーを作成する

匿名レイヤーは、宣言ごとに個別のレイヤーを作成します。次のコードではbaseレイヤーを2つのブロック構文に分けて記述しています。この場合、2つ目のブロック構文のスタイルは、既存のレイヤーに追加されます(既存のレイヤーにスタイルを追加するを参照)。

css
@layer base {
  .hoge { color: red }
}
@layer base {
  p { color: blue }
}
/* 以下と同等
@layer base {
  .hoge { color: red }
  p { color: blue }
}
*/

しかし、匿名レイヤーの場合は、個々の独立したレイヤーとして見なされます。

css
/* 別々のレイヤーと見なされる */
@layer {
  .hoge { color: red }
}
@layer {
  p { color: blue }
}
/* 以下と同等ではない!!
@layer {
  .hoge { color: red }
  p { color: blue }
}
*/

次のような記述において、名前があるレイヤーと匿名レイヤーで、挙動が変わることがあるので注意しましょう。

css
/* これは`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を外部から操作されないようにしたい時
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が適用されます。

html
<!-- `color:red`が適用される -->
<p id="hoge" class="fuga" style="color:red">piyo</p>
css
@layer {
  #hoge.fuga { color: blue }
}

!importantとの比較

レイヤー化されたスタイルと!importantを付与されたスタイルとの比較では、!importantを付与されたスタイルが優先されます。

css
.hoge { color: red }
@layer base {
  /* これが優先される */
  .hoge { color: green !important }
}
@layer overwirte {
  .hoge { color: blue }
}

たとえば上記の例では、各レイヤーは以下の順でソートされます。

  1. baseレイヤー
  2. overwirteレイヤー
  3. (暗黙的な外縁レイヤー)

ただし、!importantが付与されたスタイルは、カスケードレイヤーよりも優先順位が高いため、上記ではcolor: green !importantが優先されるようになります。

!important同士の比較はカスケードレイヤーの優先順位を逆転させる

これはうっかりしているとハマりそうな挙動ですが、!important同士の比較ではカスケードレイヤーの優先順位が逆転します。

実際に例をみて確認してみましょう。ややこしいことに、次の例ではcolor: green !importantが適用されます。

html
<p class="hoge">fuga</p>
css
@layer base, overwrite;
.hoge { color: red !important }
@layer base {
  /* この記述が適用される!! */
  .hoge { color: green !important }
}
@layer overwrite {
  .hoge { color: blue !important }
}

何が起きているのか簡単に説明します。まず、!importantのことは一旦忘れて、上記のコードにおけるレイヤーの順序を確認してみます。

  1. baseレイヤー
  2. overwriteレイヤー
  3. (暗黙的な外縁レイヤー)

ここまでは本記事において何度かやってきたことであり、特に問題はありません。通常では暗黙的な外縁レイヤーが優先されるのですが、!important同士の比較では、レイヤーの優先順位が逆転します。つまりレイヤーの順序が次のようにソートされます。

  1. (暗黙的な外縁レイヤー) - !important
  2. overwriteレイヤー - !important
  3. baseレイヤー - !important

したがって、baseレイヤーの!importantの優先順位が最も高くなり、上記ではcolor: green !importantが適用されることとなります。

おわりに

いかがでしたでしょうか。カスケードレイヤーと向き合う話でした。案外ややこしい部分もあったので、導入する際には、事前に知識を整理しておく必要があるように感じました。カスケードレイヤーは便利である一方で、その影響範囲も大きいので、やはりCSS設計をしっかり考えることが重要ですね。

参考

https://www.w3.org/TR/css-cascade-5/#layering
https://developer.mozilla.org/ja/docs/Web/CSS/@layer
https://css-tricks.com/css-cascade-layers/

Discussion

ログインするとコメントできます