このシリーズでは、CSS の重要な概念であるカスケード(Cascade)の基礎を改めて学び直し、その応用としてカスケードレイヤー(@layer)について説明していきます。

今回は CSS カスケードの仕組みを説明する基礎編です。

2024/03/26 追記

記事公開当初、カスケードレイヤー(@layer)に関して「Un-Layered」と「Un-named Layer」を混同した誤った説明をしておりました。訂正してお詫びいたします。

正しくは、「Un-Layered」は @layer を使用しない従来のスタイル指定のことで、「Un-named Layer」はレイヤー名を明示せずに @layer を使用する「無名レイヤー」や「匿名レイヤー」などと呼ばれる指定のことです。

カスケード(Cascade)は「階段状に連続する滝」という意味ですが、 CSS(Cascading Style Sheets)では段階的に評価するアルゴリズムが存在しており、同じ要素に対してスタイルが競合すると、より優先度の高い指定によって上書きされます。

この優先度については詳細度(Specificity)が注目されがちですが、新たに導入されたカスケードレイヤーを含めていくつかの段階に分かれており、詳細度はその一部でしかありません。

順を追って説明していきますが、以下はそのカスケードの全体像を表現したウィジェットです。

Widget

CSS Cascade

  • Origins
    • Transitions
    • User-Agent
      !important
    • User
      !important
    • Author
      !important
    • Animations
    • Author
    • User
    • User-Agent
  • Context
    • Shadow
      !important
    • Host
      !important
    • Host
    • Shadow
  • Layers, Inline Styles
    • Inline Style
      !important
    • First Layer
      !important
    • Last Layer
      !important
    • Un-Layered
      !important
    • Inline Style
    • Un-Layered
    • Last Layer
    • First Layer
  • Specificity
    • ID
      1-0-0
    • CLASS
      0-1-0
    • TYPE
      0-0-1
    • No Value
      0-0-0
  • Scoping Proximity
  • Order of Appearance

ウィジェットの上部には 2 つのスイッチがあり、以下の機能を持っています。

  • 「Open」を選択するとグループの下層が開く(下層がある場合)
  • 「Append !important」を選択すると、下層に !important の項目が追加される

その下の 6 つの項目が、カスケードのステップです。

評価順グループ名概要
1Originsオリジン
2ContextShadow ツリーのコンテキスト
3Layers, Inline Stylesカスケードレイヤーとインラインスタイル
4Specificity詳細度
5Scoping Proximity@scope の近接性
6Order of Appearance出現順

「Layers, Inline Styles」は、W3C の仕様書では「Element-Attached Styles」と「Layers」にそれぞれ分かれていますが、!important の有無によって優先順位が前後し相互に影響するため、1 つのグループにまとめています。

これらの基準は上から順番に評価され、優先順位が同じ場合には下の基準へと遷移していきます。最後まで優劣がつかない場合には出現順(Order of Appearance)で決まります。

まずはオリジン(Origins)です。オリジンのみを抜き出したウィジェットを以下に表示します。

Widget

CSS Cascade

  • Origins
    • Transitions
    • User-Agent
      !important
    • User
      !important
    • Author
      !important
    • Animations
    • Author
    • User
    • User-Agent

先頭の「Transitions」と「Animations」は、transitionanimation が発生している間だけ適用される仮想のオリジンですが、ここでは気にせずに次の項目に進みます。

その下の 3 つのオリジン「Author」「User」「User-Agent」に注目します。

  • Author
  • User
  • User-Agent

Author オリジンは、ウェブサイトやウェブアプリの制作者が作成したスタイルシートであり、style.css ファイルや <style> 要素、style 属性で記述したスタイルのすべてが該当します。つまり、われわれが普段書いている CSS を指します。

User オリジンは、ブラウザを使用してウェブサイトを閲覧しているユーザが記述したスタイルシートです。拡張機能経由で使用したり、ブラウザにスタイルシートを読み込んでカスタマイズする方法が考えられます。

User-Agent オリジンは、ブラウザのデフォルトのスタイルシートです。例えば、すべての HTML 要素の display プロパティの初期値は inline ですが1、通常は User-Agent スタイルシートによって <p><div> といった要素に display: block が指定されています。

Google Chrome のデベロッパーツールで User-Agent のスタイルを確認した状態のスクリーンショット。`<p>` 要素に指定されているルールセットを表示している
Google Chrome のデベロッパーツールで User-Agent のスタイルを確認した状態

この 3 つのオリジンは上から「Author > User > User-Agent」の順番で優先されるため、「User」や「User-Agent」のスタイルは「Author」で上書きすることができます。

!important フラグ

この見出しのリンク

続いて、!important フラグを指定した状態を含めた優先順位を確認します。

  • User-Agent !important
  • User !important
  • Author !important
  • Author
  • User
  • User-Agent

ここで注目する点は、通常の状態と比較して順位が反転しているということです。このルールによって「User-Agent !important」が最優先されることがわかります。

例えば、User-Agent スタイルシートでは、<audio> 要素に以下の指定があります。

Google Chrome の User-Agent スタイルシートから抜粋
audio:not([controls]) {
  display: none !important;
}

このコードは、controls 属性を指定していない <audio> 要素は絶対に表示されないことを意味します。つまり、Author スタイルシートや User スタイルシートで !important フラグを使用したとしても display の値を上書きすることはできません2

このように、!imoprtant フラグの役割は重要度を上げるだけではなく、カスケードの優先順位を反転させる効果があることを理解しておきましょう。

注意点としては、スタイルが管理しづらくなるので、Author スタイルシートにおいては !imoprtant フラグの使用はなるべく控えましょう。明確な意図があるときのみ使用すべきで、その理由をコメントとして残しておくのが望ましいです。

最後に「Transitions」と「Animations」も含めた順位を確認しておきます。

  • Transitions
  • User-Agent !important
  • User !important
  • Author !important
  • Animations
  • Author
  • User
  • User-Agent

オリジンの優先度が同一で決着がつかない場合には、次のステップに移動します。

revert は、スタイルをリセットする目的で使用されることの多いキーワードですが、このキーワードを指定すると、オリジンの段階をロールバックする(差し戻す)ことができるため、現在のオリジンの指定が存在しないかのように振る舞います。

revert キーワードの例
<!-- HTML -->
<ul class="foo">
  <li>Lorem ipsum</li>
</ul>

<!-- CSS -->
<style>
ul {
  list-style-type: none;
}

/* 通常は User-Agent オリジンまで戻り、`list-style-type: disc` が適用される */
ul.foo {
  list-style-type: revert;
}
</style>

Author オリジンで使用した場合、まずは User オリジンまで戻りますが、User オリジンに該当する指定がなければ User-Agent オリジンまで戻ります。

もし、User-Agent オリジンにも該当する指定がなければ、unset を指定したのと同様に扱われます。

コンテキスト(Context)は、Shadow DOM を使用したときの Shadow ツリーに対する基準です。Shadow DOM ではない場合には次のステップに進みます。

Widget

CSS Cascade

  • Context
    • Shadow
      !important
    • Host
      !important
    • Host
    • Shadow

この「Host」が何を指しているかですが、Shadow ツリーの外側から ::part() 擬似要素を使用してスタイルを指定するケースが該当します。

以下のコードを例にすると、Shadow DOM で指定している background-color: black は、::part() で指定している background-color: teal で上書きされます。

Shadow DOM の例
<!-- HTML -->
<my-shadow></my-shadow>

<!-- CSS -->
<style>
my-shadow::part(text) {
  background-color: teal;
}
</style>

<!-- JS -->
<script>
class MyShadow extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    const style = document.createElement('style');
    shadow.innerHTML = `
      <style>
        [part="text"] {
          background-color: black;
          color: white;
        }
      </style>
      <p part="text">Lorem ipsum</p>
    `;
  }
}
customElements.define('my-shadow', MyShadow);
</script>

つまり、通常では内側のコンテキスト(Shadow)よりも、外側のコンテキスト(Host)のほうが強いということになります。

そして、先ほどのオリジン同様に !important フラグを使用すると順位が反転します。

  • Shadow !important
  • Host !important
  • Host
  • Shadow

続いて、カスケードレイヤー(@layer)とインラインスタイルです。

Widget

CSS Cascade

  • Layers, Inline Styles
    • Inline Style
      !important
    • First Layer
      !important
    • Last Layer
      !important
    • Un-Layered
      !important
    • Inline Style
    • Un-Layered
    • Last Layer
    • First Layer

インラインスタイルについては説明するまでもないですが、HTML 要素に対して style 属性でスタイルを指定した場合が該当します。

インラインスタイルの例
<p style="color: green;">Lorem ipsum</p>

それ以外の 3 つのレイヤーを見ていきましょう。

  • Un-Layered
  • Last Layer
  • First Layer

「Un-Layered」とは、カスケードレイヤー(@layer)を使用しない従来のルールセットです。

「Last Layer」「First Layer」はそれぞれ、最後に指定したレイヤーと最初に指定したレイヤーを意味しています。作成するレイヤーの数に制限はないですが、説明をシンプルにするために最初と最後の 2 つのレイヤーのみを対象としています。

具体的な CSS のコードを見てみましょう。

カスケードレイヤーの例
/* レイヤーの優先順位を定義 */
@layer first, last;

/* Un-Layered */
p {
  color: teal;
}

/* First Layer */
@layer first {
  p {
    color: blue;
  }
}

/* Last Layer */
@layer last {
  p {
    color: green;
  }
}

Un-Layered に加え、@layerfirstlast の 2 つのレイヤーを定義していますが、この場合は Un-Layered が優先されるので、color: teal が適用されます。

また、これまでと同様に !important フラグを付与すると順序が反転します。

  • First Layer !important
  • Last Layer !important
  • Un-Layered !important
  • Un-Layered
  • Last Layer
  • First Layer

先ほどの CSS コードの各宣言に !important フラグを追加すると、「First Layer」が一番強くなるので、color: blue が適用されます。

!imoprtant フラグを追加した例
/* Un-Layered */
p {
  color: teal !important;
}

/* First Layer */
@layer first {
  p {
    color: blue !important;
  }
}

/* Last Layer */
@layer last {
  p {
    color: green !important;
  }
}

最後に「Inline Style」も含めた順位を確認しておきます。

  • Inline Style !important
  • First Layer !important
  • Last Layer !important
  • Un-Layered !important
  • Inline Style
  • Un-Layered
  • Last Layer
  • First Layer

なお、カスケードレイヤー(@layer)については、次回の記事で詳しく取り上げます。

詳細度(Specificity)は、セレクタの種類やその数によって優先度を決めるおなじみのルールですが、この段階になってようやく登場します。

Widget

CSS Cascade

  • Specificity
    • ID
      1-0-0
    • CLASS
      0-1-0
    • TYPE
      0-0-1
    • No Value
      0-0-0

この記事では詳しくは取り上げませんが、以下のルールでウェイト(優先度)が計算されます。

ID セレクタのウェイトは 1-0-0 です。#foo のようなセレクタが対象です。

クラス、擬似クラス、属性セレクタのウェイトは 0-1-0 です。.bar:hover[type="radio"] のようなセレクタが対象です。

要素、擬似要素のウェイトは 0-0-1 です。p::after のようなセレクタが対象です。

全称セレクタ(*)、そして :where() 擬似クラスで指定したセレクタのウェイトは加算されないので 0-0-0 です。


これらのセレクタの組み合わせで詳細度が決まります。以下のように同じ要素に対するスタイルの指定であっても、セレクタの記述によってはウェイトが大きく異なります。

詳細度の例
<!-- HTML -->
<p id="foo" class="bar">
  <a href="#">Lorem ipsum</a>
</p>

<!-- CSS -->
<style>
/* 詳細度: 1-2-1 */
#foo.bar a:hover {
  color: green;
}

/* 詳細度: 0-1-0 */
:where(#foo.bar) > *:hover {
  color: blue;
}
</style>

また、ウェイトは左の列から右の列に段階的に評価され、勝敗が決まった時点で確定します。

つまり、ID セレクタが 1 つだけで、クラスセレクタが仮に 10 個(またはそれ以上)指定されていたとしても、最初の段階で勝敗が決まるので ID セレクタが優先されます。

ウェイトは左から右に評価される
<!-- HTML -->
<p id="foo" class="bar baz">
  Lorem ipsum
</p>

<!-- CSS -->
<style>
/* 詳細度: 1-0-0 */
#foo {
  color: green;
}

/* 詳細度: 0-10-0 */
.bar.baz.bar.baz.bar.baz.bar.baz.bar.baz {
  color: blue;
}
</style>

「Scoping Proximity」とはスコープの近接性のことで、スタイルが競合しているときにスコープ(@scope)が近いほうの要素が優先されるルールです。

具体的には、@scope 規則をネストしているときに、詳細度(Specificity)までの優先度が同じであれば、このスコープの近接性が判断材料になります。

@scope 規則は W3C の仕様では「CSS Cascading and Inheritance Level 6」に属しており、草案(Working Draft)段階のため、実装方法や機能に変更が発生する可能性があります。また、現時点では主要ブラウザのなかでは Firefox が非対応ですが、Safari は 17.4 でサポートされました 🎉

@scope をネストしている例
<!-- HTML -->
<div class="foo">
  <p>Foo</p>
  <div class="bar">
    <p>Bar</p>
    <div class="foo">
      <p>Foo</p>
    </div>
  </div>
</div>

<!-- CSS -->
<style>
@scope (.foo) {
  p {
    color: green;
  }
}
@scope (.bar) {
  p {
    color: blue;
    font-weight: bold;
  }
}
</style>

このとき、3 番目に出現する最下層の .foo は、親要素である .bar から継承した font-weight: bold が適用されますが、詳細度が同じ color プロパティについては、近接性のルールから color: green が適用されます。

ちなみに、この近接性は @scope 規則でのみ評価されるので、それ以外の通常のセレクタにおいては影響を受けません。前述のコードのスタイルを以下のように置き換えると、最下層の .foo の近接性は考慮されずに .bar から継承した color: blue が適用されます。

通常のセレクタでは近接性は評価されない
.foo {
  p {
    color: green;
  }
}
.bar {
  p {
    color: blue;
    font-weight: bold;
  }
}

ここまでで勝敗がついていない場合には、要素の出現順(Order of Appearance)で決まります。特筆すべきことはありませんが、より後ろに指定したスタイルが適用されます。

カスケードの評価基準は何段階にも分かれており、カスケードレイヤーやスコープの近接性のような新たな概念も加わっているため、一見すると複雑で難しく感じます。

しかし、これらの概念を正しく理解することで、スタイルの優先度が管理しやすくなり、より柔軟かつ堅牢なコードを維持することができるようになるでしょう。

ここで改めてカスケードの全体像を表現したウィジェットを掲載します。この記事をとおして、CSS カスケードへの理解の一助になっていれば幸いです。

Widget

CSS Cascade

  • Origins
    • Transitions
    • User-Agent
      !important
    • User
      !important
    • Author
      !important
    • Animations
    • Author
    • User
    • User-Agent
  • Context
    • Shadow
      !important
    • Host
      !important
    • Host
    • Shadow
  • Layers, Inline Styles
    • Inline Style
      !important
    • First Layer
      !important
    • Last Layer
      !important
    • Un-Layered
      !important
    • Inline Style
    • Un-Layered
    • Last Layer
    • First Layer
  • Specificity
    • ID
      1-0-0
    • CLASS
      0-1-0
    • TYPE
      0-0-1
    • No Value
      0-0-0
  • Scoping Proximity
  • Order of Appearance

次回はさらに進めて、優先度を制御する仕組みである、カスケードレイヤー(@layer)について説明していきます。

本記事の作成にあたり、以下のウェブページを参考にしました。

脚注

  1. Box Layout Modes: the display property | W3C」を確認すると、初期値が inline であることがわかります。

  2. controls 属性が指定されていなければ表示するものがないので当然といえば当然です。意図しない表示を防止するためのフールプルーフだと考えることができます。