Monday, October 13, 2025
HomeWeb developmentCSS nesting improves with CSSNestedDeclarations  |  Articles  |  internet.dev

CSS nesting improves with CSSNestedDeclarations  |  Articles  |  internet.dev


Bramus

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 additional CSSStyleRule 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 a CSSStyleDeclaration occasion representing the declarations.
  • The cssRules property which is a CSSRuleList that holds all nested CSSRule 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
}
RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments