公開日: 2024 年 10 月 8 日
CSS のネストの奇妙な点を修正するため、CSS ワーキング グループは CSSNestedDeclarations
インターフェースを CSS ネスト仕様に追加することを決断しました。この追加により、スタイルルールの後に続く宣言が上に移動しなくなるなど、いくつかの改善が行われています。
これらの変更は Chrome バージョン 130 以降で利用可能で、Firefox Nightly 132 と Safari Technology Preview 204 でテストできます。
対応ブラウザ
CSSNestedDeclarations
を使用しない CSS ネストの問題
CSS のネストに関する注意点の一つとして、元々は次のスニペットが想定どおりに機能しないことがあります。
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
コードを見ると、background-color: green;
宣言が最後に来るため、<div class=foo>
要素に green
background-color
があると想定されます。しかし、バージョン 130 より前の Chrome ではそうなりません。CSSNestedDeclarations
をサポートしていないこれらのバージョンでは、要素の background-color
は red
です。
実際のルールを解析した結果、Chrome が 130 回使用する前は次のようになります。
.foo {
width: fit-content;
background-color: green;
@media screen {
& {
background-color: red;
}
}
}
解析後の CSS には 2 つの変更が加えられました。
background-color: green;
がシフトされて、他の 2 つの宣言を結合します。- ネストされた
CSSMediaRule
は、&
セレクタを使用して宣言を追加のCSSStyleRule
でラップするように書き換えられました。
もう一つの一般的な変更は、パーサーがサポートしていないプロパティを破棄することです。
CSSStyleRule
から cssText
を読み戻すことで、「解析後の CSS」を検査できます。
こちらのインタラクティブなプレイグラウンドで、実際に試してみましょう。
この CSS が書き換えられた理由
この内部書き換えが発生した理由を理解するには、この CSSStyleRule
が CSS オブジェクト モデル(CSSOM)でどのように表されるかを理解する必要があります。
130 より前の Chrome では、先ほど共有した CSS スニペットは次のようにシリアル化されます。
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = ".foo"
.resolvedSelectorText = ".foo"
.specificity = "(0,1,0)"
.style (CSSStyleDeclaration, 2) =
- width: fit-content
- background-color: green
.cssRules (CSSRuleList, 1) =
↳ CSSMediaRule
.type = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = "&"
.resolvedSelectorText = ":is(.foo)"
.specificity = "(0,1,0)"
.style (CSSStyleDeclaration, 1) =
- background-color: red
CSSStyleRule
のすべてのプロパティのうち、このケースに関連するのは次の 2 つです。
style
プロパティ。宣言を表すCSSStyleDeclaration
インスタンスです。cssRules
プロパティ。これは、ネストされたすべてのCSSRule
オブジェクトを保持するCSSRuleList
です。
CSS スニペットのすべての宣言が CSStyleRule
の style
プロパティに格納されるため、情報が失われます。style
プロパティを見ると、background-color: green
がネストされた CSSMediaRule
の後に宣言されていることが明確ではありません。
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = ".foo"
.style (CSSStyleDeclaration, 2) =
- width: fit-content
- background-color: green
.cssRules (CSSRuleList, 1) =
↳ …
これは問題です。CSS エンジンが正しく機能するには、スタイルルールの内容の先頭に表示されるプロパティと、他のルールに散在するプロパティを区別できる必要があります。
CSSMediaRule
内の宣言が突然 CSSStyleRule
でラップされる理由は、CSSMediaRule
が宣言を含むように設計されていないためです。
CSSMediaRule
にはネストされたルールを含めることができ、cssRules
プロパティからアクセスできるため、宣言は自動的に CSSStyleRule
でラップされます。
↳ CSSMediaRule
.type = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = "&"
.resolvedSelectorText = ":is(.foo)"
.specificity = "(0,1,0)"
.style (CSSStyleDeclaration, 1) =
- background-color: red
解決方法
CSS ワーキング グループは、この問題を解決するためのいくつかのオプションを検討しました。
提案された解決策の一つは、ネスト セレクタ(&
)を使用して、すべての裸の宣言をネストされた CSSStyleRule
でラップすることでしたが、&
が :is(…)
にデスガーリングされた場合に次のような望ましくない副作用が生じる可能性があるため、このアイデアはさまざまな理由で却下されました。
- 特異性に影響します。これは、最も具体的な引数の特異性を
:is()
が引き継ぐためです。 - 元の外側セレクタの疑似要素ではうまく機能しません。これは、
:is()
がセレクタリストの引数で疑似要素を受け入れないためです。
次の場合について考えてみましょう。
#foo, .foo, .foo::before {
width: fit-content;
background-color: red;
@media screen {
background-color: green;
}
}
このスニペットを解析すると、Chrome 130 より前のバージョンでは次のようになります。
#foo,
.foo,
.foo::before {
width: fit-content;
background-color: red;
@media screen {
& {
background-color: green;
}
}
}
これは、&
セレクタでネストされた CSSRule
が原因で問題が発生します。
:is(#foo, .foo)
にフラット化され、その過程でセレクタリストから.foo::before
が破棄されます。- 特定度が
(1,0,0)
であるため、後で上書きしにくくなります。
これは、ルールがシリアル化された内容を調べることで確認できます。
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = "#foo, .foo, .foo::before"
.resolvedSelectorText = "#foo, .foo, .foo::before"
.specificity = (1,0,0),(0,1,0),(0,1,1)
.style (CSSStyleDeclaration, 2) =
- width: fit-content
- background-color: red
.cssRules (CSSRuleList, 1) =
↳ CSSMediaRule
.type = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = "&"
.resolvedSelectorText = ":is(#foo, .foo, .foo::before)"
.specificity = (1,0,0)
.style (CSSStyleDeclaration, 1) =
- background-color: green
視覚的には、.foo::before
の background-color
が green
ではなく red
であることを意味します。
CSS ワーキング グループが検討したもう 1 つのアプローチは、ネストされた宣言をすべて @nest
ルールでラップすることです。デベロッパー エクスペリエンスの低下が原因で、この機能は却下されました。
CSSNestedDeclarations
インターフェースの概要
CSS Working Group が決定した解決策は、ネストされた宣言ルールの導入です。
このネストされた宣言ルールは、Chrome 130 以降の Chrome で実装されています。
対応ブラウザ
ネストされた宣言ルールの導入により、CSS パーサーが変更され、連続して直接ネストされた宣言が CSSNestedDeclarations
インスタンスに自動的にラップされるようになりました。この CSSNestedDeclarations
インスタンスは、シリアル化されると CSSStyleRule
の cssRules
プロパティに格納されます。
次の CSSStyleRule
を例に説明します。
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
Chrome 130 以降でシリアル化すると、次のように表示されます。
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = ".foo"
.resolvedSelectorText = ".foo"
.specificity = (0,1,0)
.style (CSSStyleDeclaration, 1) =
- width: fit-content
.cssRules (CSSRuleList, 2) =
↳ CSSMediaRule
.type = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSNestedDeclarations
.style (CSSStyleDeclaration, 1) =
- background-color: red
↳ CSSNestedDeclarations
.style (CSSStyleDeclaration, 1) =
- background-color: green
CSSNestedDeclarations
ルールは CSSRuleList
で終了するため、パーサーは background-color: green
宣言の位置を保持できます。この位置は、background-color: red
宣言(CSSMediaRule
の一部)の後です。
さらに、CSSNestedDeclarations
インスタンスを使用すると、破棄された他の潜在的な解決策によって引き起こされた厄介な副作用は発生しません。ネストされた宣言ルールは、親スタイルルールと同じ要素と疑似要素と一致し、同じ詳細度動作をします。
CSSStyleRule
の cssText
を読み取ることで確認できます。ネストされた宣言ルールにより、入力 CSS と同じになります。
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
クリエイターへの影響
つまり、Chrome 130 以降、CSS のネスト機能が大幅に改善されました。ただし、ネストされたルールと単純な宣言を交互に使用していた場合は、コードの一部を確認しなければならない可能性があります。
次に、優れた @starting-style
を使用する例を示します。
/* This does not work in Chrome 130 */
#mypopover:popover-open {
@starting-style {
opacity: 0;
scale: 0.5;
}
opacity: 1;
scale: 1;
}
Chrome 130 より前は、これらの宣言はホイスティングされていました。最終的には、opacity: 1;
宣言と scale: 1;
宣言が CSSStyleRule.style
に配置され、その後に CSSStyleRule.cssRules
に CSSStartingStyleRule
(@starting-style
ルールを表す)が配置されます。
Chrome 130 以降では、宣言はホイスティングされなくなり、CSSStyleRule.cssRules
に 2 つのネストされた CSSRule
オブジェクトが作成されます。順番に、1 つの CSSStartingStyleRule
(@starting-style
ルールを表す)と、opacity: 1; scale: 1;
宣言を含む 1 つの CSSNestedDeclarations
です。
この動作の変更により、@starting-style
宣言は CSSNestedDeclarations
インスタンスに含まれる宣言によって上書きされ、エントリ アニメーションが削除されます。
コードを修正するには、@starting-style
ブロックが通常の宣言の後ろにあることを確認します。次に例を示します。
/* This works in Chrome 130 */
#mypopover:popover-open {
opacity: 1;
scale: 1;
@starting-style {
opacity: 0;
scale: 0.5;
}
}
CSS ネストを利用する場合は、ネストされた宣言をネストされたルールの上に配置すると、CSS ネストをサポートするすべてのブラウザのすべてのバージョンでコードがほとんど正常に動作します。
最後に、CSSNestedDeclarations
が使用可能であることを特徴検出する場合は、次の JavaScript スニペットを使用できます。
if (!("CSSNestedDeclarations" in self && "style" in CSSNestedDeclarations.prototype)) {
// CSSNestedDeclarations is not available
}