1. Introduction
This section is not normative.
This module describes support for nesting a style rule within another style rule, allowing the inner rule’s selector to reference the elements matched by the outer rule. This feature allows related styles to be aggregated into a single structure within the CSS document, improving readability and maintainability.
1.1. Module Interactions
This module introduces new parser rules that extend the [CSS21] parser model. This module introduces selectors that extend the [SELECTORS4] module.
1.2. Values
This specification does not define any new properties or values.
1.3. Motivation
The CSS for even moderately complicated web pages often include lots of duplication for the purpose of styling related content. For example, here is a portion of the CSS markup for one version of the [CSS-COLOR-3] module:
table.colortable td{ text-align : center; } table.colortable td.c{ text-transform : uppercase; } table.colortable td:first-child, table.colortable td:first-child+td{ border : 1 px solid black; } table.colortable th{ text-align : center; background : black; color : white; }
Nesting allows the grouping of related style rules, like this:
table.colortable{ & td{ text-align : center; &.c{ text-transform : uppercase} &:first-child, &:first-child + td{ border : 1 px solid black} } & th{ text-align : center; background : black; color : white; } }
Besides removing duplication, the grouping of related rules improves the readability and maintainability of the resulting CSS.
2. Nesting Style Rules
Style rules can be nested inside of other styles rules.
These nested style rules act exactly like ordinary style rules—
There are two closely-related syntaxes for creating nested style rules:
-
Direct nesting, where the nested style rule is written normally inside the parent rule, but with the requirement that the nested style rule’s selector is nest-prefixed.
-
The @nest rule, which imposes less constraints on the nested style rule’s selector.
Aside from the slight difference in how they’re written, both methods are exactly equivalent in functionality.
Why can’t everything be directly nested?
Nesting style rules naively inside of other style rules is, unfortunately, ambiguous—
For example, if a parser starts by seeing color:hover ...,
it can’t tell whether that’s the color property
(being set to an invalid value...)
or a selector for a <color>
element.
It can’t even rely on looking for valid properties to tell the difference;
this would cause parsing to depend on which properties the implementation supported,
and could change over time.
Requiring directly-nested style rules to use nest-prefixed selectors
works around this problem—
Some non-browser implementations of nested rules do not impose this requirement. It is, in most cases, eventually possible to tell properties and selectors apart, but doing so requires unbounded lookahead in the parser; that is, the parser might have to hold onto an unknown amount of content before it can tell which way it’s supposed to be interpreting it. CSS to date requires only a small, known amount of lookahead in its parsing, which allows for more efficient parsing algorithms, so unbounded lookahead is generally considered unacceptable among browser implementations of CSS.
2.1. Direct Nesting
A style rule can be directly nested within another style rule if its selector is nest-prefixed.
To be nest-prefixed, a nesting selector must be the first simple selector in the first compound selector of the selector. If the selector is a list of selectors, every complex selector in the list must be nest-prefixed for the selector as a whole to be nest-prefixed.
/* & can be used on its own */ .foo{ color : blue; & > .bar{ color : red; } } /* equivalent to .foo { color: blue; } .foo > .bar { color: red; } */ /* or in a compound selector, refining the parent’s selector */ .foo{ color : blue; &.bar{ color : red; } } /* equivalent to .foo { color: blue; } .foo.bar { color: red; } */ /* multiple selectors in the list must all start with & */ .foo, .bar{ color : blue; & + .baz, &.qux{ color : red; } } /* equivalent to .foo, .bar { color: blue; } :is(.foo, .bar) + .baz, :is(.foo, .bar).qux { color: red; } */ /* & can be used multiple times in a single selector */ .foo{ color : blue; & .bar & .baz & .qux{ color : red; } } /* equivalent to .foo { color: blue; } .foo .bar .foo .baz .foo .qux { color: red; } */ /* Somewhat silly, but can be used all on its own, as well. */ .foo{ color : blue; &{ padding : 2 ch ; } } /* equivalent to .foo { color: blue; } .foo { padding: 2ch; } // or .foo { color: blue; padding: 2ch; } */ /* Again, silly, but can even be doubled up. */ .foo{ color : blue; &&{ padding : 2 ch ; } } /* equivalent to .foo { color: blue; } .foo.foo { padding: 2ch; } */ /* The parent selector can be arbitrarily complicated */ .error, #404{ &:hover > .baz{ color : red; } } /* equivalent to :is(.error, #404):hover > .baz { color: red; } */ /* As can the nested selector */ .foo{ &:is ( .bar, &.baz) { color : red; } } /* equivalent to .foo:is(.bar, .foo.baz) { color: red; } */ /* Multiple levels of nesting "stack up" the selectors */ figure{ margin : 0 ; & > figcaption{ background : hsl ( 0 0 % 0 % /50 % ); & > p{ font-size : .9 rem ; } } } /* equivalent to figure { margin: 0; } figure > figcaption { background: hsl(0 0% 0% / 50%); } figure > figcaption > p { font-size: .9rem; } */
But these are not:
/* No & at all */ .foo{ color : blue; .bar{ color : red; } } /* & isn’t the first simple selector */ .foo{ color : blue; .bar&{ color : red; } } /* & isn’t the first selector of every one in the list */ .foo, .bar{ color : blue; & + .baz, .qux{ color : red; } }
The last example here isn’t technically ambiguous,
since the selector as a whole does start with an &,
but it’s an editing hazard—
For example, if one component uses the class .foo, and a nested component uses .foo__bar, you could write this in Sass as:
.foo{ color : blue; &__bar{ color : red; } } /* In Sass, this is equivalent to .foo { color: blue; } .foo__bar { color: red; } */
Unfortunately, this method is inconsistent with selector syntax in general, and at best requires heuristics tuned to particularly selector-writing practices to recognize when the author wants it, versus the author attempting to add a type selector in the nested rule. __bar, for example, is a valid custom element name in HTML.
As such, CSS can’t do this; the nested selector components are interpreted on their own, and not "concatenated":
.foo{ color : blue; &__bar{ color : red; } } /* In CSS, this is instead equivalent to .foo { color: blue; } __bar.foo { color: red; } */
2.2. The Nesting At-Rule: @nest
While direct nesting looks nice, it is somewhat fragile. Some valid nesting selectors, like .foo &, are disallowed, and editing the selector in certain ways can make the rule invalid unexpectedly. As well, some authors find the nesting challenging to distinguish visually from the surrounding declarations.
To aid in all these issues, this specification defines the @nest rule, which imposes fewer restrictions on how to validly nest style rules. Its syntax is:
@nest = @nest <selector-list> { <style-block> }
The @nest rule is only valid inside of a style rule. If used in any other context (particularly, at the top-level of a stylesheet) the rule is invalid.
The @nest rule functions identically to a nested style rule: it starts with a selector, and contains a block of declarations that apply to the elements the selector matches. That block is treated identically to a style rule’s block, so anything valid in a style rule (such as additional @nest rules) is also valid here.
The only difference between @nest and a directly nested style rule is that the selector used in a @nest rule is less constrained: it only must be nest-containing, which means it contains a nesting selector in it somewhere, rather than requiring it to be at the start of each selector. A list of selectors is nest-containing if all of its individual complex selectors are nest-containing.
.foo{ color : red; @nest & > .bar{ color : blue; } } /* equivalent to .foo { color: red; } .foo > .bar { color: blue; } */
But @nest allows selectors that don’t start with an &, so the following are also valid:
.foo{ color : red; @nest .parent &{ color : blue; } } /* equivalent to .foo { color: red; } .parent .foo { color: blue; } */ .foo{ color : red; @nest :not ( &) { color : blue; } } /* equivalent to .foo { color: red; } :not(.foo) { color: blue; } */
But the following are invalid:
.foo{ color : red; @nest .bar{ color : blue; } } /* Invalid because there’s no nesting selector */ .foo{ color : red; @nest & .bar, .baz{ color : blue; } } /* Invalid because not all selectors in the list contain a nesting selector */
.foo{ color : blue; @nest .bar &{ color : red; &.baz{ color : green; } } } /* equivalent to .foo { color: blue; } .bar .foo { color: red; } .bar .foo.baz { color: green; }
2.3. Nesting Conditional Rules
In addition to @nest rules and directly nested style rules, this specification allows nested conditional group rules inside of style rules.
When nested in this way, the contents of a conditional group rule are parsed as <style-block> rather than <stylesheet>:
-
Properties can be directly used, applying to the same elements as the parent rule (when the conditional group rule matches)
-
Style rules are treated as directly nested, and so must have nest-prefixed selectors, with their nesting selector taking its definition from the nearest ancestor style rule.
-
@nest rules are allowed, again with their nesting selector taking its definition from the nearest ancestor style rule.
Note: This implies that "normal" style rules, without a nesting selector, are invalid in a nested conditional group rule.
/* Properties can be directly used */ .foo{ display : grid; @media ( orientation: landscape) { grid-auto-flow : column; } } /* equivalent to .foo { display: grid; @media (orientation: landscape) { & { grid-auto-flow: column; } } } */ /* finally equivalent to .foo { display: grid; } @media (orientation: landscape) { .foo { grid-auto-flow: column; } } */ /* Conditionals can be further nested */ .foo{ display : grid; @media ( orientation: landscape) { grid-auto-flow : column; @media ( min-width >1024 px ) { max-inline-size : 1024 px ; } } } /* equivalent to .foo { display: grid; } @media (orientation: landscape) { .foo { grid-auto-flow: column; } } @media (orientation: landscape) and (min-width > 1024px) { .foo { max-inline-size: 1024px; } } */
But the following are invalid:
.foo{ color : red; @media ( min-width:480 px ) { & h1, h2{ color : blue; } } } /* Invalid because not all selectors in the list contain a nesting selector */ .foo{ color : red; @nest @media ( min-width:480 px ) { &{ color : blue; } } } /* Invalid because @nest expects a selector prelude, instead a conditional group rule was provided */
2.4. Mixing Nesting Rules and Declarations
When a style rule contains both declarations and nested style rules or nested conditional group rules, the declarations must come first, followed by the nested rules. Declarations occuring after a nested rule are invalid and ignored.
article{ color : green; &{ color : blue; } color: red; }
The color: red declaration is invalid and ignored, since it occurs after the nested style rule.
However, further nested rules are still valid, as in this example:
article{ color : green; &{ color : blue; } color: red; &.foo{ color : yellow; } /* valid! */ }
For the purpose of determining the Order Of Appearance, nested style rules and nested conditional group rules are considered to come after their parent rule.
article{ color : blue; &{ color : red; } }
Both declarations have the same specificity (0,0,1), but the nested rule is considered to come after its parent rule, so the color: red declarations wins the cascade.
On the other hand, in this example:
article{ color : blue; @nest :where ( &) { color : red; } }
The :where() pseudoclass reduces the specificity of the nesting selector to 0, so the color: red declaration now has a specificity of (0,0,0), and loses to the color: blue declaration before "Order Of Appearance" comes into consideration.
3. Nesting Selector: the & selector
When using a nested style rule, one must be able to refer to the elements matched by the parent rule; that is, after all, the entire point of nesting. To accomplish that, this specification defines a new selector, the nesting selector, written as & (U+0026 AMPERSAND).
When used in the selector of a nested style rule, the nesting selector represents the elements matched by the parent rule. When used in any other context, it represents nothing. (That is, it’s valid, but matches no elements.)
a, b{ & c{ color : blue; } }
is equivalent to
:is ( a, b) c{ color : blue; }
The specificity of the nesting selector is equal to the largest specificity among the complex selectors in the parent style rule’s selector list (identical to the behavior of :is()).
#a, b{ & c{ color : blue; } } .foo c{ color : red; }
Then in a DOM structure like
< b class = foo > < c > Blue text</ c > </ b >
The text will be blue, rather than red. The specificity of the & is the larger of the specificities of #a ([1,0,0]) and b ([0,0,1]), so it’s [1,0,0], and the entire & c selector thus has specificity [1,0,1], which is larger than the specificity of .foo c ([0,1,1]).
Notably, this is different than the result you’d get if the nesting were manually expanded out into non-nested rules, since the color: blue declaration would then be matching due to the b c selector ([0,0,2]) rather than #a c ([1,0,1]).
Why is the specificity different than non-nested rules?
The nesting selector intentionally uses the same specificity rules as the :is() pseudoclass, which just uses the largest specificity among its arguments, rather than tracking which selector actually matched.
This is required for performance reasons; if a selector has multiple possible specificities, depending on how precisely it was matched, it makes selector matching much more complicated and slower.
That skirts the question, tho: why do we define & in terms of :is()? Some non-browser implementations of Nesting-like functionality do not desugar to :is(), largely because they predate the introduction of :is() as well. Instead, they desugar directly; however, this comes with its own significant problems, as some (reasonably common) cases can accidentally produce massive selectors, due to the exponential explosion of possibilities.
.a1, .a2, .a3{ & .b1, & .b3, & .b3{ & .c1, & .c2, & .c3{ ...; } } } /* naively desugars to */ .a1 .b1 .c1, .a1 .b1 .c2, .a1 .b1 .c3, .a1 .b2 .c1, .a1 .b2 .c2, .a1 .b2 .c3, .a1 .b3 .c1, .a1 .b3 .c2, .a1 .b3 .c3, .a2 .b1 .c1, .a2 .b1 .c2, .a2 .b1 .c3, .a2 .b2 .c1, .a2 .b2 .c2, .a2 .b2 .c3, .a2 .b3 .c1, .a2 .b3 .c2, .a2 .b3 .c3, .a3 .b1 .c1, .a3 .b1 .c2, .a3 .b1 .c3, .a3 .b2 .c1, .a3 .b2 .c2, .a3 .b2 .c3, .a3 .b3 .c1, .a3 .b3 .c2, .a3 .b3 .c3{ ...}
Here, three levels of nesting, each with three selectors in their lists, produced 27 desugared selectors. Adding more selectors to the lists, adding more levels of nesting, or making the nested rules more complex can make a relatively small rule expand into multiple megabytes of selectors (or much, much more!).
Some CSS tools avoid the worst of this by heuristically discarding some variations, so they don’t have to output as much but are still probably correct, but that’s not an option available to UAs.
Desugaring with :is() instead eliminates this problem entirely, at the cost of making specificity slightly less useful, which was judged a reasonable trade-off.
The nesting selector is allowed anywhere in a compound selector, even before a type selector, violating the normal restrictions on ordering within a compound selector.
div
element".
It could also be written as div& with the same meaning, but that wouldn’t be valid to start a directly nested style rule.
4. CSSOM
4.1. Modifications to CSSStyleRule
CSS style rules gain the ability to have nested rules:
partial interface CSSStyleRule { [SameObject ]readonly attribute CSSRuleList cssRules ;unsigned long insertRule (CSSOMString ,
rule optional unsigned long = 0);
index undefined deleteRule (unsigned long ); };
index
The cssRules
attribute
must return a CSSRuleList
object for the child CSS rules.
The insertRule(rule, index)
method
must return the result of
invoking insert a CSS rule rule into the child CSS rules at index.
The deleteRule(index)
method
must remove a CSS rule from the child CSS rules at index.
4.2. The CSSNestingRule
Interface
The CSSNestingRule
interfaces represents a @nest rule:
[Exposed =Window ]interface :
CSSNestingRule CSSRule {attribute CSSOMString selectorText ; [SameObject ,PutForwards =cssText ]readonly attribute CSSStyleDeclaration style ; [SameObject ]readonly attribute CSSRuleList cssRules ;unsigned long insertRule (CSSOMString ,
rule optional unsigned long = 0);
index undefined deleteRule (unsigned long ); };
index
The selectorText
attribute,
on getting,
must return the result of serializing the associated selector list.
On setting the selectorText
attribute these steps must be run:
-
Run the parse a group of selectors algorithm on the given value.
-
If the algorithm returns a non-null value replace the associated selector list with the returned value.
-
Otherwise, if the algorithm returns a null value, do nothing.
The style
attribute
must return a CSSStyleDeclaration
object for the style rule,
with the following properties:
- computed flag
-
Unset.
- declarations
-
The declared declarations in the rule, in specified order.
- parent CSS rule
-
The this object.
- owner node
-
Null.
The cssRules
attribute
must return a CSSRuleList
object for the child CSS rules.
The insertRule(rule, index)
method
must return the result of
invoking insert a CSS rule rule into the child CSS rules at index.
The deleteRule(index)
method
must remove a CSS rule from the child CSS rules at index.
To serialize a CSSNestingRule
:
return the result of the following steps:
- Let s initially be the string "
@nest
" followed by a single SPACE (U+0020). - Append to s the result of performing serialize a group of selectors on the rule’s associated selectors,
followed by the string "
{
", i.e., a single SPACE (U+0020), followed by LEFT CURLY BRACKET (U+007B). - Let decls be the result of performing serialize a CSS declaration block on the rule’s associated declarations, or null if there are no such declarations.
- Let rules be the result of performing serialize a CSS rule on each rule in the rule’s
cssRules
list, or null if there are no such rules. - If decls and rules are both null, append " }" to s (i.e. a single SPACE (U+0020) followed by RIGHT CURLY BRACKET (U+007D)) and return s.
-
If rules is null:
- Append a single SPACE (U+0020) to s
- Append decls to s
- Append " }" to s (i.e. a single SPACE (U+0020) followed by RIGHT CURLY BRACKET (U+007D)).
- Return s.
-
Otherwise:
- If decls is not null, prepend it to rules.
-
For each rule in rules:
- Append a newline followed by two spaces to s.
- Append rule to s.
- Append a newline followed by RIGHT CURLY BRACKET (U+007D) to s.
- Return s.