Printed: Oct 8, 2024
To repair some bizarre quirks with CSS nesting, the CSS Working Group resolved so as to add the CSSNestedDeclarations
interface to the CSS Nesting Specification. With this addition, declarations that come after model guidelines now not shift up, amongst another enhancements.
These modifications can be found in Chrome from model 130 and are prepared for testing in Firefox Nightly 132 and Safari Know-how Preview 204.
The issue with CSS nesting with out CSSNestedDeclarations
Considered one of the gotchas with CSS nesting is that, initially, the next snippet doesn’t work as you would possibly initially anticipate:
.foo {
width: fit-content;
@media display {
background-color: crimson;
}
background-color: inexperienced;
}
Wanting on the code, you’d assume that the <div class=foo>
ingredient has a inexperienced
background-color
as a result of the background-color: inexperienced;
declaration comes final. However this is not the case in Chrome earlier than model 130. In these variations, which lack assist for CSSNestedDeclarations
, the background-color
of the ingredient is crimson
.
After parsing the precise rule Chrome previous to 130 makes use of is as follows:
.foo {
width: fit-content;
background-color: inexperienced;
@media display {
& {
background-color: crimson;
}
}
}
The CSS after parsing underwent two modifications:
- The
background-color: inexperienced;
received shifted as much as be a part of the opposite two declarations. - The nested
CSSMediaRule
was rewritten to wrap its declarations in an additionalCSSStyleRule
utilizing the&
selector.
One other typical change that you simply’d see right here is the parser discarding properties it doesn’t assist.
You may examine the “CSS after parsing” for your self by studying again the cssText
from the CSSStyleRule
.
Attempt it out your self in this interactive playground:
Why is that this CSS rewritten?
To grasp why this inside rewrite occurred, you want to perceive how this CSSStyleRule
will get represented within the CSS Object Mannequin (CSSOM).
In Chrome earlier than 130, the CSS snippet shared earlier serializes to the next:
↳ CSSStyleRule
.kind = STYLE_RULE
.selectorText = ".foo"
.resolvedSelectorText = ".foo"
.specificity = "(0,1,0)"
.model (CSSStyleDeclaration, 2) =
- width: fit-content
- background-color: inexperienced
.cssRules (CSSRuleList, 1) =
↳ CSSMediaRule
.kind = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSStyleRule
.kind = STYLE_RULE
.selectorText = "&"
.resolvedSelectorText = ":is(.foo)"
.specificity = "(0,1,0)"
.model (CSSStyleDeclaration, 1) =
- background-color: crimson
Of all of the properties {that a} CSSStyleRule
has, the next two are related on this case:
- The
model
property which is aCSSStyleDeclaration
occasion representing the declarations. - The
cssRules
property which is aCSSRuleList
that holds all nestedCSSRule
objects.
As a result of all declarations from the CSS snippet find yourself within the model
property of the CSStyleRule
, there’s a lack of info. When wanting on the model
property it is not clear that the background-color: inexperienced
was declared after the nested CSSMediaRule
.
↳ CSSStyleRule
.kind = STYLE_RULE
.selectorText = ".foo"
.model (CSSStyleDeclaration, 2) =
- width: fit-content
- background-color: inexperienced
.cssRules (CSSRuleList, 1) =
↳ …
That is problematic, as a result of for a CSS engine to work correctly it should have the ability to distinguish properties that seem at the beginning of a method rule’s contents from those who seem interspersed with different guidelines.
As for the declarations contained in the CSSMediaRule
abruptly getting wrapped in a CSSStyleRule
: that’s as a result of the CSSMediaRule
was not designed to include declarations.
As a result of CSSMediaRule
can include nested guidelines–accessible by way of its cssRules
property–the declarations robotically get wrapped in a CSSStyleRule
.
↳ CSSMediaRule
.kind = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSStyleRule
.kind = STYLE_RULE
.selectorText = "&"
.resolvedSelectorText = ":is(.foo)"
.specificity = "(0,1,0)"
.model (CSSStyleDeclaration, 1) =
- background-color: crimson
The right way to remedy this?
The CSS Working Group seemed into a number of choices to resolve this downside.
One of many advised options was to wrap all naked declarations in a nested CSSStyleRule
with the nesting selector (&
). This concept was discarded for varied causes, together with the next undesirable side-effects of &
desugaring to :is(…)
:
- It has an impact on specificity. It’s because
:is()
takes over the specificity of its most particular argument. - It doesn’t work effectively with pseudo-elements within the unique outer selector. It’s because
:is()
doesn’t settle for pseudo-elements in its selector listing argument.
Take the next instance:
#foo, .foo, .foo::earlier than {
width: fit-content;
background-color: crimson;
@media display {
background-color: inexperienced;
}
}
After parsing that snippet turns into this in Chrome earlier than 130:
#foo,
.foo,
.foo::earlier than {
width: fit-content;
background-color: crimson;
@media display {
& {
background-color: inexperienced;
}
}
}
This can be a downside as a result of the nested CSSRule
with the &
selector:
- Flattens right down to
:is(#foo, .foo)
, throwing away the.foo::earlier than
from the selector listing alongside the way in which. - Has a specificity of
(1,0,0)
which makes it more durable to overwrite afterward.
You may test this by inspecting what the rule serializes to:
↳ CSSStyleRule
.kind = STYLE_RULE
.selectorText = "#foo, .foo, .foo::earlier than"
.resolvedSelectorText = "#foo, .foo, .foo::earlier than"
.specificity = (1,0,0),(0,1,0),(0,1,1)
.model (CSSStyleDeclaration, 2) =
- width: fit-content
- background-color: crimson
.cssRules (CSSRuleList, 1) =
↳ CSSMediaRule
.kind = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSStyleRule
.kind = STYLE_RULE
.selectorText = "&"
.resolvedSelectorText = ":is(#foo, .foo, .foo::earlier than)"
.specificity = (1,0,0)
.model (CSSStyleDeclaration, 1) =
- background-color: inexperienced
Visually it additionally signifies that the background-color
of .foo::earlier than
is crimson
as a substitute of inexperienced
.
One other strategy the CSS Working Group checked out was to have you ever wrap all nested declarations in a @nest
rule. This was dismissed as a result of regressed developer expertise this might trigger.
Introducing the CSSNestedDeclarations
interface
The answer the CSS Working Group settled on is the introduction of the nested declarations rule.
This nested declarations rule is applied in Chrome beginning with Chrome 130.
The introduction of the nested declarations rule modifications the CSS parser to robotically wrap consecutive directly-nested declarations in a CSSNestedDeclarations
occasion. When serialized, this CSSNestedDeclarations
occasion leads to the cssRules
property of the CSSStyleRule
.
Taking the next CSSStyleRule
for instance once more:
.foo {
width: fit-content;
@media display {
background-color: crimson;
}
background-color: inexperienced;
}
When serialized in Chrome 130 or newer, it seems to be like this:
↳ CSSStyleRule
.kind = STYLE_RULE
.selectorText = ".foo"
.resolvedSelectorText = ".foo"
.specificity = (0,1,0)
.model (CSSStyleDeclaration, 1) =
- width: fit-content
.cssRules (CSSRuleList, 2) =
↳ CSSMediaRule
.kind = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSNestedDeclarations
.model (CSSStyleDeclaration, 1) =
- background-color: crimson
↳ CSSNestedDeclarations
.model (CSSStyleDeclaration, 1) =
- background-color: inexperienced
As a result of the CSSNestedDeclarations
rule leads to the CSSRuleList
, the parser is ready to retain the place of the background-color: inexperienced
declaration: after the background-color: crimson
declaration (which is a part of the CSSMediaRule
).
Moreover, having a CSSNestedDeclarations
occasion would not introduce any of the nasty side-effects the opposite, now discarded, potential options prompted: The nested declarations rule matches the very same parts and pseudo-elements as its guardian model rule, with the identical specificity conduct.
Proof of that is studying again the cssText
of the CSSStyleRule
. Because of the nested declarations rule it’s the similar because the enter CSS:
.foo {
width: fit-content;
@media display {
background-color: crimson;
}
background-color: inexperienced;
}
What this implies for you
Because of this CSS nesting received a complete lot higher as of Chrome 130. However, it additionally signifies that you may need to go over a few of your code for those who had been interleaving naked declarations with nested guidelines.
Take the next instance that makes use of the great @starting-style
/* This doesn't work in Chrome 130 */
#mypopover:popover-open {
@starting-style {
opacity: 0;
scale: 0.5;
}
opacity: 1;
scale: 1;
}
Earlier than Chrome 130 these declarations would get hoisted. You’d find yourself with the opacity: 1;
and scale: 1;
declarations going into the CSSStyleRule.model
, adopted by a CSSStartingStyleRule
(representing the @starting-style
rule) in CSSStyleRule.cssRules
.
From Chrome 130 onwards the declarations now not get hoisted, and you find yourself with two nested CSSRule
objects in CSSStyleRule.cssRules
. So as: one CSSStartingStyleRule
(representing the @starting-style
rule) and one CSSNestedDeclarations
that accommodates the opacity: 1; scale: 1;
declarations.
Due to this modified conduct, the @starting-style
declarations get overwritten by those contained within the CSSNestedDeclarations
occasion, thereby eradicating the entry animation.
To repair the code, guarantee that the @starting-style
block comes after the common declarations. Like so:
/* This works in Chrome 130 */
#mypopover:popover-open {
opacity: 1;
scale: 1;
@starting-style {
opacity: 0;
scale: 0.5;
}
}
In case you preserve your nested declarations on prime of the nested guidelines when utilizing CSS nesting your code works principally high-quality with all variations of all browsers that assist CSS nesting.
Lastly, if you wish to function detect the out there of CSSNestedDeclarations
, you should utilize the next JavaScript snippet:
if (!("CSSNestedDeclarations" in self && "model" in CSSNestedDeclarations.prototype)) {
// CSSNestedDeclarations shouldn't be out there
}