Closed fantasai closed 1 year ago
[ @LeaVerou, @jensimmons, @bradkemper, @tabatkins, @mirisuzanne and I discussed this problem today; this comment is a summary of the discussions.]
The following options outline the available syntax space for nested style rules:
The first option has the problems outlined above. The second option increases the indentation level, which CSSWG had concluded was objectionable. So that leaves us the third option.
Regardless, once we're in rule-parsing mode, in order to make nested style rules compatible with style rules elsewhere, the desire is to allow both:
@scope
and :has()
(which essentially imply a prefixed &
)&.foo
or .foo &
In terms of syntactic triggers for a rule-parsing mode, the following options were considered:
&&
or @
.@nest;
@nest;
@nest;
@nest;
) but not any block @rulesAfter the trigger, parsing would continue in rule-parsing mode and not in declaration-parsing mode, so &-prefixing would no longer be required for subsequent nested style rules.
Forwards compatibility considerations:
Backwards compatibility considerations:
The recommended option from the discussions today was to allow as a trigger both:
@nest;
as a convenient convention)Pros/cons of triggering on &-prefixed rules:
@nest
everywherePros/cons of triggering on @nest
:
Pros/cons of triggering on new delimiter:
@nest
everywhereAgenda+ to discuss
Pulling out for clarity, the suggested change is:
&
, it switches to an "at-rules and style rules" mode. (This at-rule does not have to be valid to trigger this, as otherwise it means your nested rules might not work due to an unrelated at-rule not being supported yet. In the Syntax spec I'll literally just trigger it upon seeing the at-keyword token.)<relative-selector-list>
. They no longer require an &
, at the start or anywhere. They work with the existing Sass/etc logic - if an &
is mentioned in the selector, and the selector doesn't start with a combinator, it's used as-is; otherwise, it's assumed to be a relative selector chaining off of &
. That is, .foo
is interpreted as & .foo
, + .foo
is interpreted as & + .foo
, but .foo &
is left as-is.@nest
, but just as a no-op rule that can be used to trigger the "see an at-rule" switch, if your first nested selector doesn't or can't start with an &
. CSSOM will use this if necessary when serializing, if the first rule in its .cssRules list is a CSSStyleRule.Taking the following example Sass from the original Nesting issue:
.main-nav {
display: flex;
ul {
list-style-type: none;
li {
margin: 0;
padding: 0;
}
a {
display: flex;
padding: 0.5em 1em;
}
}
nav& {
display: block;
}
}
You'd write this in the new proposal as one of the following:
.main-nav {
display: flex;
& ul {
list-style-type: none;
& li {
margin: 0;
padding: 0;
}
a {
display: flex;
padding: 0.5em 1em;
}
}
nav& {
display: block;
}
}
or
.main-nav {
display: flex;
@nest;
ul {
list-style-type: none;
@nest;
li {
margin: 0;
padding: 0;
}
a {
display: flex;
padding: 0.5em 1em;
}
}
nav& {
display: block;
}
}
Which you use is up to preference.
I think this proposal introduces several issues. A parsing switch mechanic also effects humans, code editors, ...
A person needs to have seen the parsing switch to be able to understand the code. In a reasonably large file this might be dozens of line above, way out of view. From their perspective the code looks indented as if from a conditional rule.
A syntax highlighter might not be parser based and will have difficulty with this.
Overal I think this solves some writing issues but negatively affects readability.
Diffing code is a good example I think :
margin: 0;
padding: 0;
}
- & a {
+ & b {
display: flex;
padding: 0.5em 1em;
}
vs.
margin: 0;
padding: 0;
}
- a {
+ b {
display: flex;
padding: 0.5em 1em;
}
A person needs to have seen the parsing switch to be able to understand the code. In a reasonably large file this might be dozens of line above, way out of view. From their perspective the code looks indented as if from a conditional rule.
I don't think that's actually true in practice. The code means the same thing either way. The switch just changes if that code is valid or not. And that switch is only needed where you change from declarations to nesting. In a reasonably large file, you can tell what's valid from context. If you're looking at nested selectors, then you're past the point of the switch, and you can continue using nested selectors. If you're looking at declarations, then you should see a switch as you scroll down to nested stuff.
If you are adding nested stuff right after declarations, you need to add a switch. If you're on either side of that switch already, and it's off-screen - it's pretty clear which side you are on from context.
Part of the implication of this proposal is that you never mix back-and-forth between nested rules and declarations:
Part of the implication of this proposal is that you never mix back-and-forth between nested rules and declarations:
* Declarations always come first * Once you start nesting, everything has to be nested (within that block/nesting-level)
Right, that is my main hesitation about this proposal. We get some good copy-and-paste behavior at the expense of a different copy-and-paste problem, where people who regularly paste new declarations at the end of an existing block may not get what they expect.
One thing I'd like to point out is that in the code examples @tabatkins posted, using @nest
as the parsing switch appears to be preferable, as it looks more consistent because everything is a descendant selector.
However, in real nesting use cases, the first few rules are often (though of course not always) specifying variations of the base rule, e.g. &:disabled
, &:nth-child(odd)
, &.foo
etc, so you have the ampersand there anyway. E.g.:
section {
declarations;
&.main {
declarations;
}
h1 {
declarations;
}
p {
declarations;
}
...
}
so the parsing switch often comes naturally, whereas with @nest
it needs to be explicit in all cases and adds a fair bit of noise. I'm fine with this noise being opt-in, for authors that want it, but it should not be mandatory.
Note that this is exactly how the above code would have been written in Sass (which was designed without our parsing constraints and thus is IMO the most natural syntax for this, so the closer we can get, the better).
Part of the implication of this proposal is that you never mix back-and-forth between nested rules and declarations:
* Declarations always come first * Once you start nesting, everything has to be nested (within that block/nesting-level)
Right, that is my main hesitation about this proposal. We get some good copy-and-paste behavior at the expense of a different copy-and-paste problem, where people who regularly paste new declarations at the end of an existing block may not get what they expect.
a) Interleaving declarations and rules makes code harder to read, you now have to hunt down the entire rule with all its descendants to understand how the base element is styled, so I'm fine disallowing that.
b) For that reason, I think it's a fairly uncommon practice even in contexts that allow it, such as our @page
or Sass' nesting. We can probably ask the Almanac folks for some stats on that if it would be useful.
- Con: first selector in a set of nested style rules has this special requirement, which is an odd positional requirement
I'm a bit concerned about the implications of this for editing of style sheets. In particular, it introduces cases where deleting a rule (which has the initial &
to trigger the switch) will invalidate the rules after it, if they don't have that &
trigger. For example, in the middle code block in @tabatkins's comment deleting the & li { margin: 0; padding: 0; }
rule will invalidate the rule following it.
Maybe it's something we can live with given all the constraints here, but I just wanted to point out explicitly what this implies about removing rules from style sheets in the process of editing them.
maybe we should split out concerns/feedback into separate issues?
@romainmenke said :
A person needs to have seen the parsing switch to be able to understand the code. In a reasonably large file this might be dozens of line above, way out of view. From their perspective the code looks indented as if from a conditional rule.
@mirisuzanne said :
I don't think that's actually true in practice. The code means the same thing either way. The switch just changes if that code is valid or not. And that switch is only needed where you change from declarations to nesting. In a reasonably large file, you can tell what's valid from context. If you're looking at nested selectors, then you're past the point of the switch, and you can continue using nested selectors. If you're looking at declarations, then you should see a switch as you scroll down to nested stuff.
If you are adding nested stuff right after declarations, you need to add a switch. If you're on either side of that switch already, and it's off-screen - it's pretty clear which side you are on from context.
The case I had in mind was this :
@media (min-width: 300px) {
/* a lot of css */
margin: 0;
padding: 0;
}
a { /* "a" is just "a", no nesting */
display: flex;
padding: 0.5em 1em;
}
/* a lot more css */
}
vs.
.my-component {
/* a lot of css */
margin: 0;
padding: 0;
}
a { /* "a" is ".my-component a" */
display: flex;
padding: 0.5em 1em;
}
/* a lot more css */
}
I've always seen the required &
as a useful reading aid while also solving a parser implementation detail. It makes it clear that & a
is part of something else and knowing what &
stands for is important context.
Maybe I am alone in thinking that the more verbose syntax of the current draft is actually a good feature :)
I'm a bit concerned about the implications of this for editing of style sheets. In particular, it introduces cases where deleting a rule (which has the initial & to trigger the switch) will invalidate the rules after it, if they don't have that & trigger. For example, in the middle code block in @tabatkins's comment deleting the & li { margin: 0; padding: 0; } rule will invalidate the rule following it.
I initially had this concern too. I think in practice, I would probably add the &
to the beginning of each rule as if it was required. And maybe some Linters might even require it as a best practice. Maybe a shortcut in editors to add it to the beginning of each line in a selection. If there were enough rules to make this cumbersome, then I'd switch to using an @nest (or maybe @brad) switch instead.
The point is, you could always continue to prefix each line with a &
(except when you need the & to be later in the selector), and that might even make it more obvious that you were in a nested context. And would make it easier to move rules around, delete them, etc.
Wish I was invited to this call. @romainmenke you're not alone.
Maybe I am alone in thinking that the more verbose syntax of the current draft is actually a good feature :)
But you could still be that verbose if you want to. You could still proceed each rule with an &
. You just wouldn't need @nest
at the beginning of the ones with an &
somewhere else in the selector, unless it was the first rule after any declarations.
And actually, you could still write @nest multiple times and in multiple places between rules if you wanted to (switching to a mode you are already in). So I don't see why you couldn't write it in the previous sustains if you wanted, and it should still parse the same, I think.
This also means you could write the rules in a SASS compatible way.
This also means you could write the rules in a SASS compatible way.
Also reading a lot of statements on Twitter that this change would allow you to copy/paste from Sass. But that would not be true as I understand it.
Sass and the nesting specification would still be different for complex selectors.
.a .b {
/*
might need a preceding `@nest;`
or a might need to be `@nest .c &` depending on final syntax
*/
.c & {
color: green
}
}
sass :
.c .a .b { /* "a" is a descendant of "c" */
color: green;
}
current draft :
.c :is(.a .b) { /* "a" and "c" might be the same element, or one might be an ancestor of the other */
color: green;
}
I think it is misleading to present this change to the specification as "compatible with Sass".
- Con: first selector in a set of nested style rules has this special requirement, which is an odd positional requirement
I'm a bit concerned about the implications of this for editing of style sheets. In particular, it introduces cases where deleting a rule (which has the initial
&
to trigger the switch) will invalidate the rules after it, if they don't have that&
trigger. For example, in the middle code block in @tabatkins's comment deleting the& li { margin: 0; padding: 0; }
rule will invalidate the rule following it.Maybe it's something we can live with given all the constraints here, but I just wanted to point out explicitly what this implies about removing rules from style sheets in the process of editing them.
Do note that it's far easier to debug all your nested rules suddenly not being applied, than the current situation where it's per-rule and thus leaving out a necessary &
causes much smaller regressions. I've been using the current syntax through PostCSS for years, and there have been so many times where it took me a fair bit of debugging to realize a CSS bug was caused by me forgetting the the &
out in one rule (and even longer to even spot said bug).
Also reading a lot of statements on Twitter that this change would allow you to copy/paste from Sass. But that would not be true as I understand it.
This is orthogonal to nesting syntax, and is true regardless of how the nested rules are specified.
I think what people on Twitter are rejoicing about is that this makes it easier to migrate from Sass. The edits required are now O(N) on the number of rules with children, not O(Nk) where k is the number of child rules per rule, and in practice even fewer as often the first nested rule starts with &
anyway (see my earlier comment).
I think it's pretty strange and confusing to have different requirements for the first nested rule vs the others just to avoid a single character in some cases. Also agree with the above points about making editing (eg inserting or reordering rules) more difficult. Better to just be consistent about it IMO.
I think, just like variables, people coming from SASS etc just aren't used to the syntax yet. Once it becomes standard and everyone starts using it, it'll become second nature.
There are, for sure, tradeoffs and issues with every option we've considered here. All of that is well documented in the proposal above. They all introduce potential footguns for authors, and they all come with caveats at the edges - issues that will impact some code styles more than others.
From my perspective, the priority of this proposal was a flexible and forgiving syntax. In the most common cases, it just works - and authors can migrate from existing code (both Sass and the PostCSS polyfill) easily. It's also possible to copy/paste that code to the root of the document, or into a scope rule, etc. The implications change slightly in those different cases, but in each one the code makes sense - and has the expected behavior.
From there, authors can choose to make various improvements, based on their preferred code styles. This was also very clearly true of the existing syntax - where some authors may choose to require @nest
, and others would choose not to. I agree, I likely wouldn't want to rely on 'the first &
is special' behavior - so I would discuss with my team if we want to always use &
, or always use @nest
. Either way, we would set up a linter to enforce the style we want on our projects.
Even though I wouldn't rely on it for large projects, I still think it makes sense to allow that flexibility as far as possible, so that most things will just work. It's unfortunate that as far as possible stops with the first &
, but there are similar issues with the other proposals.
We are not going to achieve a perfect syntax here that everyone loves. But we can achieve one that has the flexibility to handle most of what authors throw at it.
I understand that for professional developers flexible and forgiving is not always the priority we choose. Most of us use linters to make our code less flexible and less forgiving. We can still do that! But I believe flexible and forgiving is actually a pretty good guiding ideal for CSS itself, even if many of us will use tooling to enforce more consistent code styles.
To add to Mia's excellent responses above, it is actually fairly common for the last thing in a sequence to have more forgiving syntax, for example:
I'd argue the last n-1 things having more forgiving syntax than the first one is a fairly similar pattern to the first n-1 things having the stricter syntax and the last one having the more forgiving syntax.
Also, there are two ways to frame this:
@nest;
is mandatory; but CSS will error correct and insert it if it encounters an &-prefixed rule.I wonder if some of the people against this proposal would be more amenable to the second framing?
Though these are not necessarily entirely equivalent, there could be OM differences, depending on how we represent @nest
in the OM.
If viewing it as an error-correction why couldn't it be inserted whenever it encounters a non-literal token? That way it would only require an extraneous &
or @nest
when the first selector starts with a tag name.
If viewing it as an error-correction why couldn't it be inserted whenever it encounters a non-literal token? That way it would only require an extraneous
&
or@nest
when the first selector starts with a tag name.
I'd actually be fine with that.
I think the important thing is to be consistent. Requiring &
in some cases but not others is confusing for beginners and advanced authors alike. Unlike semicolons and braces, &
has a significant impact on the meaning of the rule. With &
, it's immediately obvious where the "context" (i.e. parent) is inserted into the selector. When it isn't required, authors must either know the rules about how it's automatically inserted or guess about it. This creates more to learn, forget, misunderstand, etc. leading to frustration and bugs.
Sure, lint rules can be invented, but why create this confusion in the first place? IMO, if we're even talking about a lint rule being needed to disable a new language feature, it's probably not a good idea. Especially since the only benefit is saving a single character per rule. Perhaps users of SASS will need to re-learn things, but the majority of CSS developers have never used SASS so I don't think this is a good argument. Plus, the nesting syntax already works differently in other ways as @romainmenke mentioned, so re-learning will be necessary either way. Clarity and consistency is far more important than SASS compatibility in my opinion.
I think it is also important to challenge the opinions that sparked this.
selectors within nested context are incompatible with those outside of nested context, and with syntax within @scope, which invites a lot of copy-paste errors when transferring code among these contexts
This is not solved by this syntax change. It can only be resolved by giving &
meaning everywhere. (that is why there is : https://github.com/w3c/csswg-drafts/issues/5745)
the requirement to include & within all the selectors in a list even beyond syntactic disambiguation (instead of allowing relative selectors) is annoying to type, easy to forget.
The only selectors that can omit &
are those that use a descendant combinator.
& .foo
-> .foo
But not all these :
-
&.foo
-
& + .foo
-
& > .foo
- & ~ .foo
~~And also not when the selector is the first non-declaration. The gains are very minimal here.~~
the requirement to include & within all the selectors in a list even beyond syntactic disambiguation (instead of allowing relative selectors) makes automated (and manual) conversion of existing code much more difficult (requires selector parsing rather than regex).
This is untrue. Regex will never be sufficient for migrations from Sass to nested CSS because complex selectors work fundamentally different.
This is also a one time event and should not be used to motivate a lasting language design.
&.foo and & .foo are easily confusable (both for reading and for typing), and so long as the latter is required for nested relative selectors this will be a commonly encountered problem
The same is true for .foo&
vs. .foo &
.
How does this proposal help in that case?
I am really trying to see the upsides of this proposal and have begon working on some sample code that compares certain aspects. Doing this so that I can get a feel for this version of nesting. https://github.com/romainmenke/nesting-proposal-changes-7834
At the moment however I am really struggling with this. I don't agree with the stated issues and I don't see them solved by the changes.
Correction
I initially overlooked the addition of relative selectors which enables > .foo
, + .foo
, ~ .foo
.
This comment has been edited to strike through the incorrect statements.
I think it is also important to challenge the opinions that sparked this.
selectors within nested context are incompatible with those outside of nested context, and with syntax within @scope, which invites a lot of copy-paste errors when transferring code among these contexts
This is not solved by this syntax change. It can only be resolved by giving
&
meaning everywhere. (that is why there is : #5745)
That only solves it one way: you can then paste from a nested context to @scope
or non-scoped contexts, but you cannot paste from @scope
into a nested context.
The only selectors that can omit
&
are those that use a descendant combinator.
& .foo
->.foo
But not all these :
&.foo
& + .foo
& > .foo
& ~ .foo
Nope, under this proposal <relative-selector>
, i.e. + .foo
, > .foo
or ~ .foo
will be perfectly valid.
&.foo and & .foo are easily confusable (both for reading and for typing), and so long as the latter is required for nested relative selectors this will be a commonly encountered problem
The same is true for
.foo&
vs..foo &
. How does this proposal help in that case?
Both .foo&
and .foo &
are far rarer than & .foo
and &.foo
which are some of the most common nested selectors.
Also, I'm having trouble following this logic. If something is confusing it justifies something else being confusing?
I am really trying to see the upsides of this proposal and have begon working on some sample code that compares certain aspects. Doing this so that I can get a feel for this version of nesting. romainmenke/nesting-proposal-changes-7834
This sample code demonstrates my earlier point that for a lot of code, no non-optional ampersands will even need to be included.
@romainmenke I took a stab at converting your sample code following this proposal to sample code with a mandatory @nest;
(since you are arguing that ampersands should not kick the parser into rule mode, that's the alternative — requiring @nest
in all rules that have children).
Is this preferable? Is it more understandable?
Is this preferable? Is it more understandable?
No this is, in my personal opinion, much worse. :)
Which is also why I doing my best to avoid @nest;
until I can find a good example of CSS code where it feels natural and can be presented as a good thing.
But in these examples @nest;
is not required because of the coding styles used:
&:focus
or &.something--modifier
).child
is equivalent to & .child
).This coding style makes it highly unlikely that you will ever need to use @nest;
.
That however doesn't make @nest;
a good thing.
since you are arguing that ampersands should not kick the parser into rule mode
Not saying that.
I am concerned about a parser switch that appears once.
With a required &
or @nest .foo &
in each selector you have more context as a reader.
Nope, under this proposal
, i.e. + .foo, > .foo or ~ .foo will be perfectly valid.
I overlooked that, thank you for pointing that out.
I wonder if we can have relative selector support in nesting without breaking the current proposal and if this would be implementable in browsers?
Both .foo& and .foo & are far rarer than & .foo and &.foo which are some of the most common nested selectors. Also, I'm having trouble following this logic. If something is confusing it justifies something else being confusing?
If it isn't a truly good solution that covers the entire problem I sometimes (not always) find it better to do nothing. This gives a result that is worse but is consistently worse instead of confusing in its own way.
If &<space>
or <space>&
is a problem that must be solved then I think there must be a better way than having the ability to omit the leading &
.
If viewing it as an error-correction why couldn't it be inserted whenever it encounters a non-literal token? That way it would only require an extraneous & or @nest when the first selector starts with a tag name.
Yeah, I've been thinking thru the parsing implications, and this should be doable. I need to be a little careful, because people today rely on "put some random symbol at the start of your property to 'comment it out'" and that needs to be preserved, but I believe I can handle this very reasonably in the spec.
Currently the parsing rules for style blocks are (ignoring the ending conditions and the one bit that's for the current Nesting spec):
To accommodate this new bit, I'd instead have:
Then "consume an ambiguous rule" is identical to the existing "consume a qualified rule", except it'll fail early if it encounters a top-level semicolon, raising a parse error and returning nothing.
I believe it's okay in practice to defer a "parse this as X or fail" decision until later in the stream, while "parse this as X or as Y" needs to be know which with small, finite lookahead (which is why "just do it like Sass" is problematic for our impls).
Note, tho, that this will slightly tie our hands in the future - we'll never be able to change the property syntax to start with a non-ident (like doing additive CSS by writing +transform: ...
or something). This probably isn't a huge deal, but it is definitely a forwards-compat/language evolution issue to worry about.
The benefit of this, btw, is that the "parsing switch" becomes almost automatic as soon as you start writing rules. The sole exception is if your first rule's selector starts with a tagname; that will require the @nest;
explicit switch. In all other cases you can simply pop rules in and not worry about it.
(And to help fix the potential author confusion for this one case, devtools can rely on arbitrary lookahead to tell whether something that's parsing as a random unknown property actually looks like a rule, and flag that as a potential error.)
this will slightly tie our hands in the future
I don't like compromising the future of the language just to avoid typing &
or @nest
when nesting. Especially when in one case you will still need to use @nest
anyways. I think the "flexible and forgiving syntax" will just cause confusion among occasional/newbie authors who don't want to learn all the intricacies of the language.
if you see anything else: if you're in "declarations mode", consume an ambiguous rule. If this succeeds, switch to "rules" mode. if you're in "rules" mode, consume a rule.
I guess recognizing comments would be in there somewhere too, like if you see a slash, look ahead another character to see if it is a *
. Comments could be in either mode.
It would be nice if we also explicitly recognized double slashes as the beginning of a comment that extends to the end of the line. Like SCSS commenting.
Does & have to be a prefix to a selector, or is it sufficient on its own?
i.e. is &{}
exactly equivalent to @nest;
?
&{} would create an empty rule (which would be visible to e.g. CSSOM), so I don't think it's exactly equivalent to @nest.
Parser starts in "declarations" mode. If you see an at-keyword, consume an at-rule. Switch to "rules" mode if not already in it. If you see an ident: if you're in "declarations" mode, consume a declaration. If you're in "rules" mode, consume a rule. if you see anything else: if you're in "declarations mode", consume an ambiguous rule. If this succeeds, switch to >"rules" mode. if you're in "rules" mode, consume a rule.
How does this round trip with serialisation when the first at-keyword
encountered forms something that browser doesn't understand?
Hypothetically @when
shipped in Safari but not in Chrome.
I am guessing it parse fine initially in both browser versions.
But won't it become invalid?
CSS source :
.foo {
color: green;
@when media(min-width: 300px) {
color: red;
}
bar:hover {
color: purple;
}
}
first time : rule.cssText
:
.foo {
color: green;
bar:hover {
color: purple;
}
}
bar:hover {}
is now invalid because it doesn't have a parser switch indicator.second time : rule.cssText
:
.foo {
color: green;
}
Just reading and writing CSS with this syntax can mutate the stylesheet. Is this ok?
I guess recognizing comments would be in there somewhere too, like if you see a slash, look ahead another character to see if it is a *. Comments could be in either mode.
Comments are handled at an earlier level in the parser; they get consumed and thrown away whenever you ask for the next token, so none of the parser algorithms see them at all.
It would be nice if we also explicitly recognized double slashes as the beginning of a comment that extends to the end of the line. Like SCSS commenting.
It would be, but likely impossible due to legacy content, unfortunately.
Does & have to be a prefix to a selector, or is it sufficient on its own?
It's a selector on its own, so yeah, & {...}
is fine.
How does this round trip with serialisation when the first at-keyword encountered forms something that browser doesn't understand?
This issue is triggered by more cases than just this - using the OM to insert rules can trigger it too.
The serialization algo for CSSStyleRule will have to handle it - if the first childRule is a CSSStyleRule with a selector that wouldn't trigger the switch, it'll first serialize an @nest;
. This'll introduce a no-op rule that didn't exist in the source, but after that it roundtrips perfectly.
I think the goal of the proposed change is to have syntax closer to SCSS and to have hopefully better developer ergonomics to avoid common programming errors. This would be via the "parser switch" model vs an &
before each rule. Let me know if I missed any other goal. :)
Pros
&
before their nested selectors will appreciate the familiarity and convenience of the new proposal. They’d also appreciate the flexibility to use more than one way of expressing the syntax.Cons
@nest;
switch in "just in case" or an "I don’t know why, I just always put it there". @nest;
or when importing third-party sheets.@nest
somewhere was a strong signal of a complex nesting scenario, which felt appropriate when reading code. Using @nest
is also pretty rare, so it really meant something when you saw it (unless a team chooses to @nest
everywhere, then it loses this specialty signal). &
.Because of all of these cons, I feel that we should at least not jump to changing the spec right now, and perhaps consider these changes for a level 2 spec, since the proposed changes are more-or-less backwards compatible.
Below is an enumeration of the possible ways to nest. The current spec has two ways (start with &
or start with @nest
), and the proposed changes make it 4 (the two original styles, @nest; …
and first-selector-starts-with-&
). I thought it would be useful to write them out so that everyone can see the actual syntax options in practice.
Current spec options for nesting
.foo {
& .bar {}
&.bar {}
& > .bar {}
&:hover {}
@nest .qux & {}
@nest .qux& {}
@nest .qux > & {}
@nest .qux &:hover {}
}
.foo {
@nest & .bar {}
@nest &.bar {}
@nest & > .bar {}
@nest &:hover {}
@nest .qux & {}
@nest .qux& {}
@nest .qux > & {}
@nest .qux &:hover {}
}
/* specialty case, nesting @media, no `&` required unless creating new nested selectors */
.foo {
--brand: hotpink;
@media (prefers-color-scheme: light) {
--brand: rebeccapurple;
}
}
Proposed options for nesting
.foo {
@nest;
.bar {}
&.bar {}
> .bar {}
&:hover {}
.qux & {}
.qux& {}
.qux > & {}
.qux &:hover {}
}
.foo {
/* moved &.bar to first selector to trigger parser switch, if it's removed the rest break..? */
&.bar {}
.bar {}
> .bar {}
&:hover {}
.qux & {}
.qux& {}
.qux > & {}
.qux &:hover {}
}
/* previous syntax options still work */
.foo {
& .bar {}
&.bar {}
& > .bar {}
&:hover {}
@nest .qux & {}
@nest .qux& {}
@nest .qux > & {}
@nest .qux &:hover {}
}
.foo {
@nest & .bar {}
@nest &.bar {}
@nest & > .bar {}
@nest &:hover {}
@nest .qux & {}
@nest .qux& {}
@nest .qux > & {}
@nest .qux &:hover {}
}
/* unchanged */
.foo {
--brand: hotpink;
@media (prefers-color-scheme: light) {
--brand: rebeccapurple;
}
}
If we end up going with the proposed syntax, I also think we should consider removing the first-selector-starts-with-&
mode and instead require @nest;
if you want to go into the alternate parsing mode. This would avoid programming errors for authors. Example: suppose you have this selector:
.foo {
&.bar {}
.baz & {}
}
And then you decide actually the .bar
rule was not needed for some reason. Then you go ahead and edit it to delete that line, resulting in:
.foo {
.baz & {}
}
This selector is now an error, because there was no rule starting with an &
and @nest
was not present in the rule or at the start of the selector block. The result could be a silently broken site where all the baz
widgets are broken mysteriously.
Removing first-selector-starts-with-&
would also make use of @nest;
purely opt-in, and have no side-effects on authors who prefer not to use it.
@chrishtr Look up at https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1270665794 - I believe we can make the detection more aggressive so those examples work just fine.
@chrishtr Look up at #7834 (comment) - I believe we can make the detection more aggressive so those examples work just fine.
Oh ok. Would all examples work though?
I think some arguments from Tab in #5738 are still very relevant against the new proposal:
@nest
nesting - the only change is the new ability introduced by @nest
to put the "&" somewhere different. The less differences there are between the two cases, the more learnable it is.(The current spec has 2 cases, I have lost track of how many different variants the new proposal has. So less learnable)
more than one &
is allowed in a nesting selector, so the presence of an &
later in the selector does not indicate that the selector shouldn't also start with an &
! This is why garden-path problems are insidious, as they indicate non-local influences; an author can start with a perfectly reasonable selector with no &
at all, relying on the implicit &
at the start, then later use an &
inside of a pseudo-class and suddenly have the selector drastically change its interpretation. (For example, changing @nest .foo .bar {...}
to @nest .foo .bar:not(&) {...}
.) One could try to come up with rules to handle this, but I assure you they won't be complete, and they will drastically complexify the syntax surface here, which is a huge code smell.
Defaulting to a descendant selector, as preprocessors do, isn't the best thing perf-wise. I'd still rather have authors intentionally use the descendant selector than opt them into it by default because it's the shortest way to write something.
And regarding to defaulting to descentant combinator, it's not clear to me at all why
.foo {
color: blue;
@nest; /* BTW, I'm not even sure if this is needed :( Would the colon suffice to switch the parser? */
:hover { color: cyan }
}
would be like
.foo { color: blue; }
.foo :hover { color: cyan; }
and not e.g.
.foo { color: blue; }
.foo:hover { color: cyan; }
I think requiring &
makes the result much easier to understand. I would only allow to omit a leading &
when the selector starts with a combinator symbol. And since the descendant combinator doesn't have any symbol of its own, then require explicit &
(or reconsider adding >>
as the proper descendant combinator).
I know that .foo:has(:hover)
defaults to the descendant combinator, but at least there this behavior is kinda implied by the :has
name, and it would be pointless if it resolved to .foo:hover
. That's not the case with nesting.
And regarding to defaulting to descentant combinator, it's not clear to me at all why
This is how Sass and every Sass-like preprocessor works. It's not new, it's precisely what authors would expect if we allowed naked nesting like this.
(The current spec has 2 cases, I have lost track of how many different variants the new proposal has. So less learnable)
Both proposals have exactly two cases, so I'm not sure what you're referring to.
If going with the proposal as originally presented, the cases are:
@nest;
before it (or any other at-rule, once those are allowed).If going with my more aggressive parsing suggestion, the first case changes from "starts its selector with &" to "starts its selector with anything but a tagname selector". The second case is identical.
One of the things that keeps nagging at me is that this proposal is a 180 on so many aspects that have been discussed before. Things that were advocated against are now suddenly advocated for. I don't understand or see what is so different about the whole of this proposal.
Nesting is a complex topic and so much has been said about this. I also do not want to bring up specific quotes (unless asked for) because I believe anyone can change their mind or point of view.
Re-reading previous threads and the great insights shared about the reasoning behind the current syntax, I just don't understand how this proposal is disregarding all that.
It feels as if a giant leap was taken by some and the reasoning behind it isn't being properly explained.
&
and privileging the descendant combinator was previously not ok@argyleink said in https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1272155603 :
I think the goal of the proposed change is to have syntax closer to SCSS and to have hopefully better developer ergonomics to avoid common programming errors. This would be via the "parser switch" model vs an & before each rule. Let me know if I missed any other goal. :)
There is another goal, to have support for relative selectors in nested selector contexts. Relative selectors sometimes (not always!) copy-paste nicely between contexts.
The logic appears to be :
.foo { bar { /* decl */ } }
and the parser algorithms don't allow that@nest;
noop to fix the parser issue.But there might be different ways to have relative selectors in nesting. I've opened https://github.com/w3c/csswg-drafts/issues/7854 to explore one possible alternative.
Pros
Folks who aren’t thrilled to add an & before their nested selectors will appreciate the familiarity and convenience of the new proposal. They’d also appreciate the flexibility to use more than one way of expressing the syntax.
This could be solved in preprocessors where the ambiguity is less of an issue. The parsers in preprocessors are of a different design.
Not saying that it can not be solved in the syntax, but syntactic sugar is what tooling is really great at.
The learning curve increases for anyone not coming from SCSS, because the previous syntax's simplicity and rigidity was a feature. Folks who do know SCSS still have things to learn, it's not a free switch. For example, BEM style nesting won't port over and most copy/paste scenarios will require throwing the
@nest;
switch in "just in case" or an "I don’t know why, I just always put it there".Increasing the flexibility also increases the potential for accidental programming errors. More options also means more cases to be aware of, or the style sheet ends up with errors. It gives some but takes some. Increased flexibility also means ramping up to more nesting styles when reading other authors’ nested styles, and increases friction to understanding style sheets between teams (or even within the same team between selectors that do or don’t have
@nest;
or when importing third-party sheets.
I think this proposal has less flexibility. It is only more flexibly when writing nested CSS from scratch. At that point you can chose between multiple options.
And copy-paste from Sass (which is a one time action) happens to be correct more often than with the current syntax.
But it places more burden on readers because they have to figure out again and again which of the modes is currently in play.
&
, doesn't start with &
@nest;
, or notIt also restricts the available options when refactoring code. Needing to alter unrelated CSS to make it valid again after removing a rule should be avoidable in the syntax design.
How does it interact with git merges? What if your colleague removes the first rule and you changed the selector of the second?
In the current specification each rule is (in)valid on its own. There is no spooky action at a distance. This makes reading easier and enables more mutations.
Below is an enumeration of the possible ways to nest. The current spec has two ways (start with & or start with @nest), and the proposed changes make it 4 (the two original styles, @nest; … and first-selector-starts-with-&). I thought it would be useful to write them out so that everyone can see the actual syntax options in practice. Current spec options for nesting
I am unsure if @nest .foo { /* ... */ }
still exists in this proposal.
I don't see it mentioned anywhere.
It seems that all of the pushback centers around requiring the first selector to start with &
(unless preceded by an @-rule) while subsequent ones don't need to.
We are converging towards a variation of the proposal where the only cases that actually require the &
are selectors that start with a tag name.
Here's a crazy idea: What if we just don't allow nested selectors to start with a tag name at all, anywhere? Then there is no different handling of the first rule which some people seem to react strongly against, and any nested rule kicks the parser into rule mode, and authors can always use :is(tag)
for the cases where they need this (or just & tagname
). With this proposal, we can probably even entirely drop @nest
(but we don't have to).
There is somewhat similar precedent in CSS: regular selectors cannot have tags anywhere in the middle, they can only start with a tag, unless :is()
/:where()
is used. Not the same thing, but there is precedent for having special parsing rules around tag selectors.
Also, if at some point in the future CSS grammars have to become LR(2) for other reasons, we can allow nested selectors to start with a tag then.
Note that this will be valid with this proposal:
.foo {
.bar strong {
}
& strong {
}
}
And regarding to defaulting to descentant combinator, it's not clear to me at all why
Not just preprocessors, but also our own :has()
, and @scope
.
I think & should be required in all nested selectors because it makes it clear where the parent selector is inserted and how the selectors are combined. Not requiring it leaves the reader guessing.
I believe this was a mistake in SASS and other pre-processors that we now have the opportunity to fix. Preprocessors could still support their existing syntax and output native nesting with the & inserted if they want. I think CSS should have the least ambiguous syntax possible, and leave the sugar to the preprocessors.
It seems that all of the pushback centers around requiring the first selector to start with & (unless preceded by an @-rule) while subsequent ones don't need to.
My pushback is because you can omit it in subsequent selectors, not because it is required in the first selectors. (just to clarify :) )
I agree with @devongovett that &
should be required in nested selectors.
But this is again a simplification of the arguments against this proposal.
@nest;
is also highly problematic in my opinion.
Too much is sacrificed for the features of this proposal (relative selectors, easy copy-paste)
Star hack. Tag names and *
?
Edit: 👉🏼 👈🏼 (from https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1282776786 )
As mentioned in #7796, there are some problems with the currently-proposed nesting syntax:
@scope
, which invites a lot of copy-paste errors when transferring code among these contexts&
within all the selectors in a list even beyond syntactic disambiguation (instead of allowing relative selectors) is annoying to type, easy to forget, and makes automated (and manual) conversion of existing code much more difficult (requires selector parsing rather than regex).&.foo
and& .foo
are easily confusable (both for reading and for typing), and so long as the latter is required for nested relative selectors this will be a commonly encountered problem