Skip to content

[css-nesting-1] & representing parent elements vs parent selector #8310

@tabatkins

Description

@tabatkins

When I wrote the Nesting spec originally, I specifically defined it so that & was an ordinary selector, that happened to match the elements matched by the parent rule. This leveraged the powers that only a live browser could have, which seemed useful. Preprocessors, unable to do the same, instead have to make their "nesting selector" more of a macro, that is substituted with the parent selector.

That is, in a rule like

.a .b {
  .c & {...}
}

In the Nesting spec, the & matches "a .b element with a .a ancestor", and the nested rule imposes an additional "...and has a .c ancestor", so it's the equivalent of .c :is(.a .b). It'll match all the following markup structures:

<div class=a>
 <div class=c>
  <span class=b></span>
 </div>
</div>

<div class=c>
 <div class=a>
  <span class=b></span>
 </div>
</div>

<div class="a c">
 <span class=b></span>
</div>

Preprocessors can't do this (or rather, couldn't do this before :is() was standardized, and still can't widely do it since support for :is() is still in the low 90%s). Expanding such a selector fully (to .a .c .b, .c .a .b, .a.c .b) would mean a combinatorial explosion in selector length, and gets drastically worse the more combinators are in either selector. (Sass has a similar situation with its @extend rule, where it uses heuristics to select only a few likely expansions, rather than all possible ones.) Instead, they substitute the selector directly, giving .c .a .b, and only matching the second of those three markup structures.

I was discussing Nesting with @nex3 a few days ago, and she expressed surprise and some dismay that Nesting had this behavior. She believes that a large amount of CSS written with Sass nesting expects and depends on the current Sass behavior, not accidentally but quite intentionally - they expect this sort of nesting pattern to be expressing "and then a .c container around the previous stuff" rather than just "and then a .c container somewhere around the specific previous elements". She believes this would actually be a significant burden to people migrating from preprocessor nesting to native nesting, and even for people who are using native Nesting fresh, believes it'll be a common point of confusion for people using this pattern.

As well, under Sass's behavior, an author can recover the current spec's behavior by wrapping the & in an :is(), like .c :is(&) (or wrapping the parent selector). However, there is no way to recover Sass's behavior from the current spec's behavior.

In the majority of cases it doesn't make a difference which way we go. The most common relative selector case (a nested selector like .c or + .c) is unchanged, and many non-relative selectors are also unchanged, such as & + & or :not(&). Only cases like the above, where there is a descendant combinator in the parent selector and a descendant combinator in the nested selector with an & following it, will change their behavior.

I know this is a pretty significant semantic change to be coming at this late time, but Natalie felt it was pretty important, and @mirisuzanne backed her up, and the arguments were reasonably convincing to me.

(Note: we'd still not be doing selector concatenation - .foo { &bar {...}} is still equivalent to bar.foo, not .foobar.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions