こんにちは! サイボウズ フロントエンドエキスパートチームの麦島です。
CSS Modern Features、最終回となる今回のテーマはCascade Sorting Orderです。
CSS適用順序を構成する仕様の増加
突然ですが、
次のようなものは広く知られており、イメージしやすいかもしれません。
- 詳細度がより高いほうが優先される
- 同じ詳細度の場合は、後に記述された宣言が優先される
- style属性による宣言は詳細度よりも優先される
!important
を付与すると、詳細度やstyle属性より優先される
しかし、昨今では適用順序に関連するCSSの機能は増えています。
たとえば、開発時に次のような問いが発生する可能性が考えられ、従来の知識だけではこれらに回答することは難しいでしょう。
- Cascade LayersとScoped Stylesを併用した場合にどちらが優先されるか
- Cascade Layersを利用した場合と利用しない場合でどちらが優先されるか
- style属性と組み合わせた場合の優先順位はどうなるか
- 詳細度が同じ場合にScoped Stylesはどう処理されるか
!important
を付与した場合の優先順位はどうなるか
これらすべてを把握していないとすぐに開発できなくなる、ということはないと思われますが、ライブラリやフレームワークがアップデートによりモダンな機能を導入するケースもあり、知らず知らずの間に新しい機能を利用することもありえます。そういった場合に適用順序を把握していないと、期待するスタイルを実現できなかったり、思わぬ不具合につながる可能性もあるでしょう。
従来の知識に加えて、本連載でここまで取り上げた機能も含めたCSSの適用順序に関する仕様をいま一度再確認してみましょう。
Cascade Sorting Order
CSSの適用順序はCascade Sorting Orderと呼ばれる仕様に基づきます。一つの要素に対して2つ以上のCSS宣言があったときに、どれを優先して適用するかはこのルールに従って判断されます。
現在勧告されているCascade Sorting Orderに関する仕様は、W3CのCSS Cascading and Inheritance Level 4で定義されています。CSSの仕様は更新のたびにLevelと呼ばれる数値が上がっていき、本連載でも紹介したCascade LayersやScoped Stylesといった機能をすべて含めた仕様はLevel 6にて策定が進められています。
本記事ではLevel 6の内容をベースに解説します
Cascade Sorting Orderの構成要素
Cascade Sorting Orderでは、次の基準に沿って、上から順に判断して適用順序を並び替える仕様となっています。
- Origin and Importance/
CSSの起源と !important
の付与 - Context/
Shadow DOM内のスタイル - The Style Attribute/
style属性での宣言 - Layers/
Cascade Layersの順序 - Specificity/詳細度
- Scope Proximity/スコープの近接性
- Order of Appearance/宣言された順序
あるスタイルが2箇所で宣言されていた場合の適用順序の判定を例に考えてみると、まずはOrigin and Importanceがチェックされ、Origin and Importance内で優劣がつかない場合には、次にContextをチェックします。これをどこかで優劣がつくまで繰り返し、いずれでも判断がつかない場合には最終的にOrder of Appearanceに従い、より後に宣言されたスタイルが優先される、といったイメージです。
従来CSSの適用順序といえば詳細度がピックアップされるケースが多かったように思います。しかし、Cascade Sorting Order全体を見てみると、詳細度は適用順序を決める中での一部分でしかなく、詳細度以外にも多くの要素が関わっていることがわかります。
以降は、Cascade Sorting Orderに定義される個々の要素について、内容を確認してみましょう。
Origin and Importance/CSS の起源と!important
の付与
!important
の付与Cascade Sorting Orderの先頭に位置し、最も優先して判断されるのがOrigin and Importanceです。名前の通り、OriginとImportanceの2つの概念が含まれるため、それぞれ確認してみましょう。
Origin/CSSの起源・定義元
CSSには、その定義がどこから来たものか起源を表すOrigin
それぞれ次のように分類されます。
- Author Origin: ページ作成者が提供するもの。普段我々が書くCSSはすべてここに属します。
- User Origin: ブラウザ利用者が独自で設定したスタイル。ブラウザの拡張機能などで設定されます。
- User-Agent Origin: ブラウザがデフォルトで提供するスタイル。
ここで重要なのが、Origin間の優先順位もこの順番と同じで次のようになる点です。
- Author Origin
- User Origin
- User-Agent Origin
これを実感できる例としては、ブラウザスタイルの上書きが挙げられます。次のような無効化されたボタンを考えてみましょう。
<button disabled>ボタン</button>
Google Chromeで確認した場合、User-Agent Originに属するブラウザ標準のスタイルにより、グレーアウトされて表示されます。
このスタイルは、ブラウザ側で次のように定義されています[1]。
button:disabled {
background-color: light-dark(rgba(239, 239, 239, 0.3), rgba(19, 1, 1, 0.3));
border-color: light-dark(rgba(118, 118, 118, 0.3), rgba(195, 195, 195, 0.3));
color: light-dark(rgba(16, 16, 16, 0.3), rgba(255, 255, 255, 0.3));
}
ここで、buttonに対して次のようなスタイルを独自で定義してみます。
button {
background-color: black;
color: white;
}
実際に見た目を確認すると、独自で定義したスタイルが優先され、ブラウザ標準のスタイルが上書きされます。
普段CSSを書いていれば、このような挙動は当たり前のように感じるかもしれません。
しかし、よく考えてみると、ブラウザ側のCSSはbutton:disabled
と宣言されており、これは詳細度で表すと0-1-1
です。一方、独自で定義したスタイルはbutton
のみを対象に宣言しているため詳細度は0-0-1
となり、ブラウザ側のスタイルよりも詳細度が低く、詳細度だけで比較した場合ではスタイルを上書きすることはできないはずです。それでも確実に上書きできるのは、Cascade Sorting OrderにおいてSpecificity
Importance/!important
の有無
Origin and ImportanceのうちImportanceに該当するのが!important
付与の有無です。
!important
については!important
を付与すると優先されるかを掘り下げてみると、!important
が該当するOrigin and Importanceの判定がCascade Sorting Orderの中で最上位に位置するから、ということがわかります。
これを把握していれば、たとえば!important
が付与されたスタイルは優先されるか?」
/* レイヤーの優先順位は layer2 → layer1 */
@layer layer1 {
.text-a {
color: red;
}
.text-b {
color: red !important;
}
}
@layer layer2 {
.text-a {
color: blue;
}
.text-b {
color: blue;
}
}
<div class="text-a">テキスト</div>
<div class="text-b">テキスト</div>
!important
が付与されているスタイルは、レイヤーの優先順位が下回っていても適用されていることがわかります。これも、Cascade Sorting Orderの中で、Origin and Importanceの判定がLayersよりも上に位置するから、と考えると納得の行く挙動ですね。
なお、!important
が付与されたスタイル同士の適用順序においては、OriginやCascade Layersなどによって少し特殊な動作をします。これについては本記事の後半で解説しています。
Context/Shadow DOM内のスタイル
Origin and Importanceで判断がつかなかった場合、次はContextが判定されます。
Contextを理解するためには、Shadow DOMでのスタイル定義について知る必要があります。
Shadow DOMはWeb Componentsを構成する要素のひとつで、DOMツリーをメインのDOMツリーとは切り離してカプセル化します。このとき、CSSについても完全に分離されます。
次の例では、Shadow DOMの内外でスタイルを定義していますが、それぞれ互いに干渉していないことがわかります。
div {
color: red !important;
}
<my-element>
<template shadowrootmode="closed">
<style>
div {
color: blue;
}
</style>
<div>Shadow DOM TEXT</div>
</template>
</my-element>
<div>DOM TEXT</div>
このとき、Shadow DOMがアタッチされている要素はShadow Hostと呼ばれ、通常の要素と同じようにスタイリングが可能です。次の例では、テキストを表示するだけのShadow DOMを持つカスタム要素に対して、ボーダーや余白を設定しています。
my-element {
display: inline-block;
border: 1px solid black;
padding: 8px;
}
<my-element>
<template shadowrootmode="closed">
<div>Shadow DOM TEXT</div>
</template>
</my-element>
しかし、Shadow DOM側で:host
擬似クラスを利用します。
先ほどと同じスタイルを:host
擬似クラスを利用して再現すると次のようになります。
<my-element>
<template shadowrootmode="closed">
<style>
:host {
display: inline-block;
border: 1px solid black;
padding: 8px;
}
</style>
<div>Shadow DOM TEXT</div>
</template>
</my-element>
Shadow Hostに対するスタイル指定の方法を2パターン紹介しましたが、ここで:host
擬似クラスのスタイル指定が両方存在していた場合、どちらが優先されるか?」
Contextの比較が生じた場合、通常では外側からの宣言が優先されます。つまり、:host
擬似クラスの指定よりも、外部からのスタイル指定が優先されます。
次の例を見てみると、外部からのスタイルが適用されていることがわかります。
my-element {
display: inline-block;
border: 5px dashed blue;
padding: 16px;
}
<template shadowrootmode="closed">
<style>
:host {
display: inline-block;
border: 1px solid black;
padding: 8px;
}
</style>
<div>Shadow DOM TEXT</div>
</template>
:host
ではデフォルトのスタイルを定義しておき、利用側で柔軟にカスタマイズすることができる、と理解しておくとよいでしょう。
The Style Attribute/style属性での宣言
続いて判定されるのがThe Style Attribute = style属性での宣言です。
<!-- style 属性でのスタイル宣言 -->
<div style="color: red;">テキスト</div>
style属性自体については従来から存在するため解説を省きますが、Cascade Layersよりも優先される点は覚えておくと良いでしょう。
なお余談ですが、仕様のLevel 4以前ではCascade Sorting Order上にThe Style Attributeという項目はなく、代わりにSpecificity
しかし、Level 5以降においてstyle属性と詳細度の間に位置する形でCascade Layersが登場したことで、従来の形で仕様を記述することは不可能となりました。結果として、Cascade Sorting Order内におけるstyle属性に関する仕様はThe Style Attributeとして独立して定義されるようになりました[3]。
Layers/Cascade Layersの順序
The Style Attribute
Cascade Layersそのものについては、本連載の
より後に宣言されたレイヤーが優先される
Cascade Layersで覚えておきたいポイントは次の2点です。
- より後に宣言されたレイヤーが優先される
- レイヤーに属さないスタイルは、明示的にレイヤーに含めたスタイルより優先される
レイヤーは宣言順が重要で、より後に宣言されたレイヤーのほうが優先されます。Cascade Sorting OrderにおいてLayersはSpecificity
また、レイヤーに含めずに宣言したスタイルに関しては、すべて
次の例の場合、.text
クラスが付与された要素のカラーは"black"となります。
@layer layer1, layer2, layer3;
/* 暗黙の最終レイヤーに属するため最優先となる */
.text {
color: black;
}
/* layer3 → layer2 → layer1 の順で優先される */
@layer layer1 {
.text {
color: red;
}
}
@layer layer2 {
.text {
color: blue;
}
}
@layer layer3 {
.text {
color: green;
}
}
サブレイヤーを含めた適用順序
Cascade Layerでは、レイヤー配下にレイヤーを含めるサブレイヤーを利用でき、これも適用順序に関与します。サブレイヤーは親レイヤーごとにグループ化された中で順序が判断されますが、通常のレイヤーと同様、こちらもより後に宣言されたほうが優先されます。
また、
次のスタイルで考えてみましょう。
@layer parent1 {
.text {
color: black;
}
@layer sub1 {
.text {
color: red;
}
}
@layer sub2 {
.text {
color: blue;
}
}
}
@layer parent2 {
.text {
color: gray;
}
@layer sub1 {
.text {
color: yellow;
}
}
@layer sub2 {
.text {
color: green;
}
}
}
この場合、レイヤーの優先順位は次の通りとなり、.text
クラスが付与された要素のカラーは"gray"となります。
- 暗黙のレイヤー
(今回の例ではスタイルは未定義) parent2
(後に宣言された親レイヤ内の暗黙のサブレイヤー) parent2.
sub2 (後に宣言された親レイヤ内の最後のレイヤー) parent2.
sub1 (後に宣言された親レイヤ内の最初のレイヤー) parent1
(先に宣言された親レイヤ内の暗黙のサブレイヤー) parent1.
sub2 (先に宣言された親レイヤ内の最後のレイヤー) parent1.
sub1 (先に宣言された親レイヤ内の最初のレイヤー)
なお、この適用順序はGoogle ChromeでのDevToolsでも確認できます
Specificity/詳細度
ここまでで適用順序が確定しなかった場合、いよいよSpecificity
CSSセレクタに含まれる内容から重み付けが行われ、詳細度が高いものから順に優先度も高くなります。
/** 詳細度 1-0-0(最も詳細度が高い) */
#id {
color: red;
}
/** 詳細度 0-1-0(2番目に詳細度が高い) */
.class {
color: blue;
}
/** 詳細度 0-0-1(最も詳細度が低い) */
div {
color: green;
}
仮に2つのCSS定義で優先度を判断するケースをCascade Sorting Order全体で考えてみると、詳細度を用いた比較が行われるまでには、ここまでに紹介した内容から次の判断がされていることになります。
- ともに
!important
が付与されている、あるいはともに付与されていない - 宣言されているOriginが同じである
- Shadow DOMを考慮した宣言コンテキストが同じである
- ともにstyle属性での宣言である、あるいはともにstyle属性での宣言ではない
- Cascade Layersにおいて宣言が属するレイヤーが同一である
CSSの適用順序をコントロールするための中心的な位置付けとして広く利用される詳細度ですが、それよりも先に多くの基準によって判断がされた上で利用されていることを把握しておくと良いでしょう。
Scope Proximity/スコープの近接性
詳細度も一致した場合には、Scope Proximity = スコープの近接性の判定が行われます。スコープの近接性とはScoped Styles@scope
)
@scope (.parent) {
p {
color: red;
}
}
@scope (.child) {
p {
color: blue;
}
}
<div class="parent">
<!-- この p 要素からは @scope (.parent) のほうが近接性が高い -->
<p>Parent Text</p>
<div class="child">
<!-- この p 要素からは @scope (.child) のほうが近接性が高い -->
<p>Child Text</p>
</div>
</div>
Cascade Sorting Orderでの観点で重要となるのは、スコープの近接性よりも詳細度のほうが優先度が高い点でしょう。該当のポイントも含めてScoped Styles自体の仕様については本連載の
!important
による優先順位の逆転
!important
はOrigin and ImportanceとしてCascade Sorting Orderの先頭で判断される旨について解説しました。しかし、!important
は単純に適用順位が上がるだけでなく、利用されるシチュエーションによっては優先順位を逆転させるケースがあります。
一般的な開発プラクティスとして!important
の濫用は避けるべきと言われることもあり、日常の開発では頻繁に出会う挙動ではないかもしれません。ただ、特殊な挙動であるがゆえに、意図せず遭遇すると判断や対処が難しい部分でもあるため、詳細を確認してみましょう。
Originと!important
Origin and Importanceの解説において、次の3つのOriginについて紹介しました。
- Author Origin
- User Origin
- User-Agent Origin
通常の優先順位はAuthor Origin → User Origin → User-Agent Originであり、詳細度などを気にすることなくブラウザ標準のスタイルシートを上書きできます。しかし、!important
が付与された宣言では優先順位が逆転し、User-Agent Origin → User Origin → Author Originの順となります。
つまり、ブラウザ標準のスタイルシートで!important
が付与されていると、ページ作成者が提供するCSSでの上書きは不可能となります。
ひとつ例を挙げると、CSSのoverlay
プロパティが該当します。overlay
プロパティはポップオーバーやダイアログといった要素で用いられ、値にauto
を指定すると要素の重なりにおいて最上位に描画されるようになります。ChromiumでのUser-Agent Originに相当するスタイルを確認してみると、<dialog>
要素のモーダル状態での表示時のスタイルとして次の宣言が含まれ、auto
に対して!important
が付与されています[5]。
dialog:modal {
overlay: auto !important;
}
この指定により、<dialog>
をモーダル状態で利用すると、最上位に表示されダイアログ以外の操作がブロックされます。そしてこのスタイルはUser-Agent Origin上で!important
が付与されているため上書きできず、ブラウザ側で動作が保証されている部分となります。
例外的にoverlay
プロパティを制御する方法としてtransition
による状態のコントロールが挙げられます。<dialog>
の非表示時にアニメーションを追加したいケースにおいて、transition
によってoverlay
プロパティの変化を遅延させることで、アニメーションが完了するまで確実に最上位に描画しておくことができます。
そして、このtransition
によるプロパティの制御も、実はCascade Sorting Orderで説明できます。Origin and ImportanceにおけるOriginには、先に挙げたAuthor Origin・
- Animation Origin
- Transition Origin
そして、これら2つのOriginと、先に述べた!important
付与の有無もすべて考慮したOriginの優先順位は次のとおりです
- Transition Origin
- User-Agent Origin
( !important
) - User Origin
( !important
) - Author Origin
( !important
) - Animation Origin
- Author Origin
- User Origin
- User-Agent Origin
transition
への指定はTransition Originに相当します。Transition Originは!important
が付与されたどのOriginよりも優先順位で上回るため、例外的に制御が可能となります。
普段の開発においてUser-Agent Originのスタイルで付与された!important
を意識することはそれほど多くありませんが、ブラウザ側が挙動を担保したいものについては!important
が付与されている可能性がある点を覚えておくと良いでしょう。
Contextと!important
Contextにおいて、:host
擬似クラスでのスタイル指定よりも、外部からのスタイル指定が優先される旨の解説をしましたが、ここでも!important
が付与されている場合には優先順位が逆転します。
Contextの例で挙げたコードをベースに、すべてのスタイルに!important
を付与してみます。
my-element {
display: inline-block !important;
border: 5px dashed blue !important;
padding: 16px !important;
}
<my-element>
<template shadowrootmode="closed">
<style>
:host {
display: inline-block !important;
border: 1px solid black !important;
padding: 8px !important;
}
</style>
<div>Shadow DOM TEXT</div>
</template>
</my-element>
ともに!important
を付与しましたが、:host
擬似クラスのスタイルが優先されるようになりました。これはつまり、:host
擬似クラスでのスタイル指定において!important
を付与すると、外部からのスタイル指定を無効化できることを意味します。
Shadow DOMにおいて確実に保証したい要件がある場合に、:host
擬似クラスと!important
を用いることで外部からの上書きを抑制し、提供時に期待する表示を担保する、といったユースケースが考えられます。
Cascade Layersと!important
Cascade Layersにおいても、!important
が付与されたスタイルにおいてはレイヤーの優先順位が逆転します。
Cascade Layersはそもそも
In the same way that cascade origins provide a balance of power between user and author styles, cascade layers provide a structured way to organize and balance concerns within a single origin.
Originの仕様において!important
が付与されている場合には優先順位が逆転し、ベースとなるUser-Agent Originが優先される旨の解説をしましたが、Cascade Layersにおいてもこれを踏襲するような仕様となっています。
通常、より後に宣言されたレイヤーが優先されますが、!important
が付与されたスタイルにおいては、より先に宣言されたレイヤーのほうが優先されます。暗黙のレイヤーなども加味してすべてを列挙すると、次のような優先順位となります
- より先に宣言されたレイヤー
( !important
) - より後に宣言されたレイヤー
( !important
) - 暗黙のレイヤー
( !important
) - 暗黙のレイヤー
- より後に宣言されたレイヤー
- より先に宣言されたレイヤー
次のような.text
クラスが付与された要素のスタイル定義を例に考えてみると、文字カラーは通常のレイヤーの優先順に従いblack
が適用されますが、文字サイズについては!important
が付与されているためレイヤーの優先順位が逆転し、最も最初に宣言されたレイヤー内の14px
が適用されます。
@layer layer1, layer2, layer3;
.text {
color: black; /* color 指定の中で優先度が最も高い */
font-size: 12px !important;
}
@layer layer1 {
.text {
color: red;
font-size: 14px !important; /* font-size 指定の中で優先度が最も高い */
}
}
@layer layer2 {
.text {
color: blue;
font-size: 16px !important;
}
}
@layer layer3 {
.text {
color: green;
font-size: 18px !important;
}
}
利用ケースとしては、User-Agent Originでの!important
宣言のように、後続のレイヤーに上書きされたくないスタイルに対しての!important
の付与が考えられます。
しかし、これは適用順序の中でもかなり強い宣言となります。もともと!important
自体、むやみに利用すべきものではないですが、Cascade Layersと組み合わせる場合はさらに慎重にすべきでしょう。仮に、最初に明示的に宣言したベースとなるレイヤー上で!important
を付与してしまうと、暗黙的なレイヤーを含む他のいずれのレイヤーでも優先順位で上回ることは不可能となります。
/** 明示的に宣言したレイヤーはこの 3 つの他に存在しないと仮定する */
@layer layer1, layer2, layer3;
.text {
color: black;
}
@layer layer1 {
.text {
/** このスタイルは暗黙のレイヤーを含む他のレイヤーからは上書きできない */
color: red !important;
}
}
@layer layer2 {
.text {
color: blue;
}
}
@layer layer3 {
.text {
color: green;
}
}
これを上書きするためには、同じレイヤー内で先に!important
を付与して宣言するか、新しく先に宣言するレイヤーを追加して!important
を付与する必要があり、容易ではありません。もし!important
を付与したくなった際には、今まで以上に
まとめ
実際のところCSSの適用順序については雰囲気で理解していた方も多いかもしれません。しかし、掘り下げてみると様々な仕様が絡み合い判定されており、広く知られている詳細度などの知識は、適用順序においてもごく一部の話であることがわかりました。また、!important
の特殊な挙動を知らずに利用すると、意図せず上書き不可能なスタイルを生み出してしまうなど、厄介なことになりかねません。しかし、Cascade Sorting Orderの仕様から理解していると、トリッキーな挙動に出会ったとしても驚くことなく対処できたり、そもそも問題自体を回避できる設計を行うこともできるでしょう。
私個人がCascade Sorting Orderについて調べた最初のきっかけは!important
でも上書きできないスタイルってあるのだろうか?」
さて、全6回でお送りしてきましたCSS Modern Featuresですが、これにて終了となります。ここまでお付き合い頂きありがとうございました!
次回からの