w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.5k stars 661 forks source link

[css-nesting] Syntax suggestion #4748

Closed proimage closed 2 years ago

proimage commented 4 years ago

This is kind of a follow-on from the closed issue #2701 and #2937.

Would it make any sense to implement native CSS nesting something like this?

body {
    background: black;
    color: white;
    ( /* note the open parentheses to indicate everything within is nested */
        main {
            background: orange;
            color: black;
        }
        p {
            font: serif;
        }
    ) /* close parentheses */
}

Or perhaps even like this (uses existing parser patterns):

body {
    background: black;
    color: white;
    @nest { /* or @nested, or @child, etc */
        main {
            background: orange;
            color: black;
        }
        p {
            font: serif;
        }
    } /* END NEST */
}

Basically, it would require any nesting to be placed within a separate grouping container, as a way to differentiate in bulk between attributes and nested selectors. Seems to me that this would have the benefit of not using the ampersand character and thus avoiding complications with pre-processors...

LeaVerou commented 2 years ago

I think I prefer my second idea from that comment:

Alternate idea: What if only the first rule needs to start with &, then we can just assume the rest are selectors and not properties. Then, if anyone wants to handle this generically (e.g. when migrating from a preprocessor), only 3 characters need to be prepended to the list of nested rules: &{}.

This gives you the best of both worlds: no need to prefix with anything for most stylesheets, only a three letter prefix if your first rule does not start with &. No @nest, no other weird @rules, no double syntax, no extra indents, and an easy to remember rule: your first nested selector must start with &.

vrubleg commented 2 years ago

The more I think about using {{ and }} for blocks with nesting only, the more I like it.

Sass example:

.table-bordered {
    > :not(caption) > * {
        border-width: 2px 0;

        > * {
            border-width: 0 2px;
        }
    }
}

It would look like this:

.table-bordered {{
    & > :not(caption) > * {{
        & {
            border-width: 2px 0;
        }

        & > * {
            border-width: 0 2px;
        }
    }}
}}

The idea of @LeaVerou is also very nice. It would allow to write code the same way, but with { and } instead of {{ and }}.

proimage commented 2 years ago

@LeaVerou wrote:

I think I prefer my second idea from that comment:

Doesn't that idea involve wrapping it all in brackets again, and thus bring us back to the double-indentation issue? That's why I liked the string-based delineators of @nested; (or @nestStart ... @nestEnd)—there's no implicit indentation needed.

@vrubleg wrote:

The more I think about using {{ and }} for blocks with nesting only, the more I like it.

One problem with consecutive double brackets like this is that they would have a conflict with server-side languages such as Twig (and I think Liquid, Handlebars, etc). While those languages typically generate HTML, they can also be called upon to generate CSS as well (in the form of in-page <style> ... </style> tags), so that could be problematic.

LeaVerou commented 2 years ago

@LeaVerou wrote:

I think I prefer my second idea from that comment:

Doesn't that idea involve wrapping it all in brackets again, and thus bring us back to the double-indentation issue? That's why I liked the string-based delineators of @nested; (or @nestStart ... @nestEnd)—there's no implicit indentation needed.

Nope, not at all. Zero extra indentation.

I'll explain with some examples.

This is fine:

.bar {
    &.baz {}
    & foo {}
    :root.quux & {}
}

This is fine:

.bar {
    & foo {}
    :root.quux & {}
    &.baz {}
}

This is NOT ok (first nested rule does not start with & and is thus ignored:

.bar {
    :root.quux & {}
    & foo {}
    &.baz {}
}

This is fine:

.bar {
    &{}
    :root.quux & {}
    & foo {}
    &.baz {}
}

This is fine:

.bar {
    &.nonexistent, :root.quux & {}
    & foo {}
    &.baz {}
}
tabatkins commented 2 years ago

[ideas about delimiter-based nesting]

I'm strongly against ideas in this form. It's a parsing mode switch, which is slightly awkward technically but not killer, but it means that the parsing context you're in is less obvious, which isn't great for authors. It's also very awkward to manipulate with the OM - are those at-rules that should show up in the OM? If so, what happens when you move them into a bad order? Or are they just syntactic indicators that don't show up in the OM at all?

[first rule needs to start with &, after that you're free to use whatever]

This hits similar problems - it's a mode switch in the parser, which means your parsing context isn't obvious from local inspection. It also has OM issues - what happens when you write a correct nesting rule, then modify it in the OM to have a different selector that doesn't start with &? You can't change how you serialize it to make it work; you'd have to change how the parent rule serializes, inserting a "phantom rule" to trigger the parsing-mode switch.

Even without the OM concerns, this is an editting footgun - if you start with a valid stylesheet with nesting, but later modify the stylesheet to change the selector or add another rule, you have to be careful that you're not making the first nested rule in a sequence no longer a valid start. This sort of positional awareness is easy to mess up (which is part of why we have coding styles like "put a comma after every entry in an array, including the last one").


Overall, these suggestions keep trying to reinvent something that's already been proven out - all existing preprocessors nest in a simple way, where rules can come after properties, at the same level, without having to pay attention to the syntax of the selector or anything like that. We're operating under one additional restriction - we need parsing to be immediately unambiguous - but that's it, and I think it's reasonable to say that we should stick as close to the proven and well-established syntax as possible while addressing that one restriction. (Arguably we have a second restriction, which is that we don't want an "implicit &", both for page-perf reasons (implicit descendant selectors aren't great) and for footgun reasons (adding an & anywhere else can change the meaning of the entire selector). But that's not relevant for this topic, which is about how to allow nested rules alongside properties.)

The current spec (allowing naked nesting if the selector starts with &, or @nest-prefixed for arbitrary selectors) does this, at the cost of requiring authors to make a local check on their selector so they know whether an @nest is required or not. My preferred alternative in this thread (just always use @nest) also does this; it's better in that authors don't have to think about their selectors, but worse in that they have to write one extra token per rule.

Nothing else in this thread has done this; every single suggestion diverges, sometimes significantly, from the nesting syntax authors are already familiar with. They also all have their own drawbacks, ranging from extra indentation, to not actually being nested at all, to having to track non-local information about parser modes, to making OM manipulation much more complicated. All of them are trying to avoid just adding @nest to every rule, but I think the drawbacks all end up significantly worse. My (small) experience here adapting existing nested styles into the various versions also bears this out - "always @nest" is trivial to adapt to, while everything else requires more work and some eyeballing.

Finally, from a theoretical purity standpoint, the less "weird" we can make parsing, the better. Any novel approaches require the Syntax spec to be modified, and make it more likely there will be incompatibilities attempting to use this syntax in any other context (nesting in style attributes, for instance). I already had to put a new rule into the Syntax spec to accommodate the current Nesting spec (branching on whether I see an & or not), and switching to "always @nest" would let me drop that and go back to the dead-simple parsing that CSS currently relies on; that makes it really attractive to me. (It also would mean I don't have to do funky things to rule serialization, deciding whether to prepend @nest or not depending on whether the selector is valid to be used nakedly.) The more regular, consistent, and predictable CSS syntax is, the better it is for both authors (they won't have to learn as many context-specific parsing rules, which CSS is already overloaded with) and us future spec writers (we won't need to engineer around our past decisions when they're awkward in a new context).

I think we should keep things simple, and keep things CSS-y. We should just use the @nest rule.

tabatkins commented 2 years ago

@vrubleg

[using {{...}} for nesting]

I've already listed the problems with that - it's either an editting hazard (people need to remember to add an extra {} if they're just doing nesting, or remove the {} when they're currently nesting but realize they want to add properties, or remember that they must use a &{...} rule to add properties) or it's just as awkward as putting @nest on every rule as you wrap every rule in its own {} block. (Or you double-indent, as would be natural for this amount of bracing.)

Unless you meant that nesting is only allowed on its own, with double-braces? This is still an editting hazard - you need to rewrite unrelated parts of the rule if you start with properties and realize you want to nest afterwards (and the same in reverse, tho you could keep it as-is with a lone .foo{{ &{...} }} nested rule). It's also an OM hazard - what happens if a rule already has properties and you .insertRule() on it, or if it already has nesting and you .setPropertyValue() on it? We'd either have to make the OM more stateful in a possibly-confusing way and throw in these cases, or magically serialize the existing properties into a phantom &{...} block, changing the OM upon reserialization.

LeaVerou commented 2 years ago

[first rule needs to start with &, after that you're free to use whatever]

This hits similar problems - it's a mode switch in the parser, which means your parsing context isn't obvious from local inspection. It also has OM issues - what happens when you write a correct nesting rule, then modify it in the OM to have a different selector that doesn't start with &? You can't change how you serialize it to make it work; you'd have to change how the parent rule serializes, inserting a "phantom rule" to trigger the parsing-mode switch.

I’m not sure how this is different from starting with a selector that starts with & and then later modifying it in the OM to one that doesn't (without adding @nest). It's the same kind of positional awareness, just larger scale (rule list vs selector list of a single rule). In fact, it could even happen with the current syntax if you rearrange the selectors in a selector list (e.g. &.button, .button-group > & to .button-group > &, &.button.

The part about serialization is annoying, but we could fix it by inserting a phantom rule regardless if there are nested rules. It's ugly, but far better to have serialization ugliness, than authoring ugliness.

Even without the OM concerns, this is an editting footgun - if you start with a valid stylesheet with nesting, but later modify the stylesheet to change the selector or add another rule, you have to be careful that you're not making the first nested rule in a sequence no longer a valid start. This sort of positional awareness is easy to mess up (which is part of why we have coding styles like "put a comma after every entry in an array, including the last one").

The main point of this suggestion is that in most cases authors wouldn't have to think about it at all, most stylesheets would just naturally work. Also editors could highlight rules where the first one didn't start with a &, in the same way they highlight improperly nested strings.

Positional awareness is everywhere, that's how parsers work! The part of a declaration that is a property value is the part that comes after a comma. The code that comes after a ( or { is different than the code before it. The code that comes after the ; in a CSS rule begins another declaration. I don't see how "the code that comes after & (with the &) begins the set of nested rules" is that different. Even with commas in arrays, nobody is making the point that every array must start and end with a comma, just to prevent forgotten commas while editing.

Overall, these suggestions keep trying to reinvent something that's already been proven out - all existing preprocessors nest in a simple way, where rules can come after properties, at the same level, without having to pay attention to the syntax of the selector or anything like that. We're operating under one additional restriction - we need parsing to be immediately unambiguous - but that's it, and I think it's reasonable to say that we should stick as close to the proven and well-established syntax as possible while addressing that one restriction.

A lot of these suggestions (including mine) are trying to do the very opposite: make preprocessor code somewhat more compatible with CSS Nesting so that migration is less painful (both in terms of migrating existing stylesheets, as well as migrating muscle memory, which is much harder).

(Arguably we have a second restriction, which is that we don't want an "implicit &", both for page-perf reasons (implicit descendant selectors aren't great) and for footgun reasons (adding an & anywhere else can change the meaning of the entire selector).

Is this a theoretical concern or has anyone actually added a & somewhere in a selector and was surprised to find its meaning change? I've been using implicit descendants in Sass for years and never had this problem nor have I heard anyone have this problem. The performance is the same if you're forced to just prepend with &, you're just hoping that it will get authors to use something else and not descendants. Given that authors already use descendants extensively even when no nesting is available and they have to type out everything by hand, I think your hopes there are a little misplaced. All that this mandatory & is doing is causing extra hassle when migrating stylesheets from existing preprocessors and when people used to preprocessors use this syntax. At this point I may even have used CSS Nesting (via PostCSS) more than I've used Sass in my life, and I still forget the & and have to debug why my rule is being dropped. The fact that you're already nesting the { ... } blocks makes it feel like the extra & is redundant unless you want to specify a different relationship. Even making it default to child would have been better than making it invalid IMO…

My preferred alternative in this thread (just always use @nest) […] My (small) experience here adapting existing nested styles into the various versions also bears this out - "always @nest" is trivial to adapt to, while everything else requires more work and some eyeballing.

I would be strongly against making the syntax even more annoying than it is today by having to prefix everything with @nest. Having used it a lot, it's already a pain to add & and @nest as needed. The cognitive overhead of deciding whether @nest is needed is orders of magnitude smaller than the overhead of writing @nest (even with autocomplete) or filtering it out mentally when reading stylesheets.

Finally, from a theoretical purity standpoint, the less "weird" we can make parsing, the better.

Designing syntax around parsing instead of people is completely the wrong kind of prioritization and a usability antipattern. Authors are above spec editors and authors are above implementors in the priority of constituencies. If certain syntax makes things significantly easier or faster for authors at the cost of complicating implementations or spec work, that in most cases is a worthy tradeoff.

tabatkins commented 2 years ago

I’m not sure how this is different from starting with a selector that starts with & and then later modifying it in the OM to one that doesn't (without adding @nest). It's the same kind of positional awareness, just larger scale (rule list vs selector list of a single rule).

That "larger scale" is the problem. The current spec just needs to have a check in the rule serialization algorithm to output the rule with @nest if the selector isn't appropriate for direct nesting. (And if we do "always @nest, we don't need any special cases at all.) Your proposal needs something significantly more complex/fiddly, based on knowing if you're the first nested rule in your parent, and possibly inserting fake rules just to satisfy the parsing concerns, which is a completely novel thing in CSS.

In fact, it could even happen with the current syntax if you rearrange the selectors in a selector list (e.g. &.button, .button-group > & to .button-group > &, &.button.

No, the requirement in the current spec is that if you're directly nested, all the selectors in the list have to be directly nestable, precisely to avoid this sort of authoring hazard.

Positional awareness is everywhere, that's how parsers work!

As the author of the Syntax spec, and the writer of a number of additional parsers for various languages, I know that. ^_^

But not all positional awareness is created equal. Knowing what parsing context you're in is important; it should be obvious and easy to determine, and ideally hard to accidentally flip. Your proposal doesn't satisfy this, I believe - it has three separate parsing contexts: properties, then a single rule starting with &, then any rule at all - and I think it's relatively easy to mess that up. Nested rules that are valid at one spot in a parent rule are invalid in another spot; removing a rule (the first one that flips the context over) can invalidate rules coming afterward; etc.

A lot of these suggestions (including mine) are trying to do the very opposite: make preprocessor code somewhat more compatible with CSS Nesting so that migration is less painful (both in terms of migrating existing stylesheets, as well as migrating muscle memory, which is much harder).

No, every one of them requires similar or more migration effort than the current spec, and most present a significantly different syntax model than what every existing nesting syntax uses

Your proposal is no different. Most Sass nesting, from what I can tell, does not start with an &; they instead usually use relative selectors that start with a combinator (including, most commonly, the descendant combinator). So authors dropping a preprocessor and moving to raw CSS will very likely have to modify their rules anyway. And, going to my earlier point, they won't be able to do so trivially either - they'll have to check if the first nested rule leads with an &, and if not, add an extra dummy rule to force it into the right parsing context.

I would be strongly against making the syntax even more annoying than it is today by having to prefix everything with @nest. Having used it a lot, it's already a pain to add & and @nest as needed. The cognitive overhead of deciding whether @nest is needed is orders of magnitude smaller than the overhead of writing @nest (even with autocomplete) or filtering it out mentally when reading stylesheets.

Then we can keep the current spec. ^_^

Designing syntax around parsing instead of people is completely the wrong kind of prioritization and a usability antipattern. Authors are above spec editors and authors are above implementors in the priority of constituencies. If certain syntax makes things significantly easier or faster for authors at the cost of complicating implementations or spec work, that in most cases is a worthy tradeoff.

That's why I explicitly called this out as a theoretical purity concern; I assumed using the term-of-art from the priority of constituencies would make it obvious that I recognize what level this concern is at.

But the priority of constituencies is not an absolute, as I'm sure you know. Theoretical purity can win out if it leads to greater overall usability or future-friendliness, even if it's somewhat less optimal in an individual situation. It's important to not throw things out a priori just because they're from a "lower" level.

My concern here is about (a) learnability and (b) extensibility to other contexts. The more things stick to "standard"/pre-existing CSS syntax constructs, the easier it is to learn and explain; every diversion from the CSS standard practice means another oddity for authors to learn and remember. As well, the more things stick to standard practice, the more likely we can use it in other CSS contexts without having to do anything special; we wouldn't even need to think about mixing it in syntactically, it will Just Work.

Most notably:

Is this a theoretical concern or has anyone actually added a & somewhere in a selector and was surprised to find its meaning change? I've been using implicit descendants in Sass for years and never had this problem nor have I heard anyone have this problem.

If we use "always @nest", then we don't have any parsing concerns, and could easily allow relative selectors instead (including implicit descendant combinator), because the prelude of an at-rule is wide-open in its syntax. We can't do that today because it would require us to add more parsing carve-outs for detecting a rule vs a property.

proimage commented 2 years ago

@LeaVerou wrote:

Nope, not at all. Zero extra indentation.

I'll explain with some examples.

  • snip *

Ahh, I had indeed misunderstood that idea. To be honest I'm not crazy about that specific implementation... nothing about the string of chars &{} says to me "This is the start of nested selectors", and the other options of just requiring the first selector to start with & while the rest don't have that requirement just seems... arbitrary? Like, & still represents the parent selector, but if it comes first in a selector line, then treat it as a delimiter instead/as well? I dunno, something bugs me about multi-purpose bits of code that have non-obvious meanings depending on a non-obvious context.

Granted, the @nested; or @nestStart ... @nestEnd delimiters don't help fix the non-obvious context issue any, but at least their meanings are more obvious to CSS authors.

@tabatkins wrote:

[ideas about delimiter-based nesting]

I'm strongly against ideas in this form. It's a parsing mode switch, which is slightly awkward technically but not killer, but it means that the parsing context you're in is less obvious, which isn't great for authors. It's also very awkward to manipulate with the OM - are those at-rules that should show up in the OM? If so, what happens when you move them into a bad order? Or are they just syntactic indicators that don't show up in the OM at all?

Hmm... let me respond to your questions with a question of my own: Given the current proposed standard of "& or @nest before every nested CSS rule" , how would those nested rules show up in the OM? Because in my admittedly non-programmer brain, delimiting a block of nested selectors and their properties with unique keywords or characters is basically like telling the parser "When @nestStart is encountered, prepend @nest in front of each selector until @nestEnd is reached." So as far as I'm concerned, any selector within a @nestStart ... @nestEnd block would be treated and interacted with in the OM exactly the same way that selectors with explicit @nest prefixes are now.

tabatkins commented 2 years ago

Given the current proposed standard of "& or @nest before every nested CSS rule" , how would those nested rules show up in the OM?

As CSSStyleRule objects in the .childRules property of the parent rule.

I'm not sure you understood my objection, tho - currently, all at-rules show up in the CSSOM as rule objects (save for @charset, which has some very bizarre behavior due to both legacy constraints and the way it works in general; it's not actually "part of CSS syntax").

It sounds like you're saying, tho, that @nestStart and @nestEnd aren't at-rules at all, but rather some new type of "parsing directive" that doesn't reflect in the OM, but just controls how the parser works. That would be a brand-new thing for CSS.

proimage commented 2 years ago

My concern here is about (a) learnability and (b) extensibility to other contexts. The more things stick to "standard"/pre-existing CSS syntax constructs, the easier it is to learn and explain; every diversion from the CSS standard practice means another oddity for authors to learn and remember. As well, the more things stick to standard practice, the more likely we can use it in other CSS contexts without having to do anything special; we wouldn't even need to think about mixing it in syntactically, it will Just Work.

To be fair, the closest proposal of all of these to "CSS standard practice" is the one that copies the pattern established by things such as @keyframes { ... } and @media () { ... }. It's the second proposal in my original post at the very start of the thread. It just adds an extra level of indentation... as do both @keyframes and @media, really. ¯\_(ツ)_/¯

tabatkins commented 2 years ago

Putting an @nest around all the nested rules, or around each nested rule, is equally close to "CSS standard practice"; both styles show up depending on the rule and its specifics.

proimage commented 2 years ago

It sounds like you're saying, tho, that @nestStart and @nestEnd aren't at-rules at all, but rather some new type of "parsing directive" that doesn't reflect in the OM, but just controls how the parser works. That would be a brand-new thing for CSS.

And the current proposed nesting standard isn't a brand-new thing for CSS? Where is this pattern used:

.selector {
    property: value;
    @something .immediately.followed-by .anotherSelector { ... }
}

I.e. I thought @at-directives were always followed by an enclosing pair of brackets.

tabatkins commented 2 years ago

No, we have several at-rules with stuff in the prelude (the space between the at-keyword and the {): @keyframes, @counter-style, @font-feature-values, etc.

Usually it's just the name of the thing being defined/altered, but that's not a requirement.

tabatkins commented 2 years ago

Or, and I don't know why this slipped my mind, @media and @supports, which put complex grammars in that space very much akin to selectors. ^_^

proimage commented 2 years ago

Right, but all those are parameters that govern when the at-rule takes effect, or metadata about the at-rule (eg. @keyframe's name). Don't all the at-rules then go on to wrap the CSS they're "governing" (or the from..to in @keyframe's case) in enclosing brackets?

proimage commented 2 years ago

Looking through https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule, it's very much a mixed bag. However, I don't think there are any at-rules that are currently valid as a "sibling" of a selector's properties, so.... 🤷

tabatkins commented 2 years ago

"parameters that govern when the at-rule takes effect" certainly describes the relationship between a selector and the declarations inside of it, yeah.

Don't all the at-rules then go on to wrap the CSS they're "governing" (or the from..to in @keyframe's case) in enclosing brackets?

Yes, but not as an intrinsic quality.

@media/@supports wraps style rules because the style rules are separate from the media/support query. In theory we could have instead had @media take a MQ then immediatley a selector, and then have its body just contain declarations, but that would be exceedingly cumbersome for authors because you almost always are using one MQ to set a large number of styles on different elements, and this syntax would require you to repeat the whole MQ on each one.

@keyframes wraps its keyframe rules because (a), again, the standard is to have a reasonably large number of keyframes, and repeating @keyframes <name> on each would be rather cumbersome, and (b) because keyframes have "last wins" behavior if multiple are defined with the same name, and that's fairly desirable (otherwise, we'd have to, I guess, merge the frames from each?). Having a bunch of separately-tagged frames wouldn't allow this.

However, I don't think there are any at-rules that are currently valid as a "sibling" of a selector's properties, so.... shrug

Yes, no at-rules are currently valid inside of a style rule. But that's, again, not an intrinsic restriction; we just haven't defined any yet to do so, since existing at-rules aren't element-specific. There are already some that absolutely make sense inside of a style rule, such as @media/@supports, and preprocessors allow this; in CSS we were just waiting for the Nesting functionality to exist before we allowed it (and the Nesting spec, thus, defines them as valid there).

proimage commented 2 years ago

@Keyframes wraps its keyframe rules because (a), again, the standard is to have a reasonably large number of keyframes, and repeating @keyframes on each would be rather cumbersome,

This argument might end up being applicable to @nest as well. Time vill tell...

There are already some that absolutely make sense inside of a style rule, such as @media/@supports, and preprocessors allow this; in CSS we were just waiting for the Nesting functionality to exist before we allowed it (and the Nesting spec, thus, defines them as valid there).

What's the syntax for those? Identical to how SCSS has them working? Or do they need to be prefixed with @nest too? ;)

...of all the people to be opposed to having too many TABS.... 🤣 😉

LeaVerou commented 2 years ago

As the author of the Syntax spec, and the writer of a number of additional parsers for various languages, I know that. ^_^

But not all positional awareness is created equal. Knowing what parsing context you're in is important; it should be obvious and easy to determine, and ideally hard to accidentally flip. Your proposal doesn't satisfy this, I believe - it has three separate parsing contexts: properties, then a single rule starting with &, then any rule at all - and I think it's relatively easy to mess that up. Nested rules that are valid at one spot in a parent rule are invalid in another spot; removing a rule (the first one that flips the context over) can invalidate rules coming afterward; etc.

I know you know that, that's why I was referencing it 😄

The way I see it, it's super easy to flip context in most cases: insert a non-escaped quote in a string and boom, you're out of the string. Insert a } in a CSS declaration and boom, you're out of the rule. Same with most delimiters.

Your proposal is no different. Most Sass nesting, from what I can tell, does not start with an &; they instead usually use relative selectors that start with a combinator (including, most commonly, the descendant combinator).

Yes but & is valid there, so they could be writing stylesheets today with the mindset of switching later.

I would be strongly against making the syntax even more annoying than it is today by having to prefix everything with @nest. Having used it a lot, it's already a pain to add & and @nest as needed. The cognitive overhead of deciding whether @nest is needed is orders of magnitude smaller than the overhead of writing @nest (even with autocomplete) or filtering it out mentally when reading stylesheets.

Then we can keep the current spec. ^_^

Yeah, I think that's the best compromise. It would certainly be unfortunate if this discussion results in more syntax, rather than less.

If we use "always @nest", then we don't have any parsing concerns, and could easily allow relative selectors instead (including implicit descendant combinator), because the prelude of an at-rule is wide-open in its syntax. We can't do that today because it would require us to add more parsing carve-outs for detecting a rule vs a property.

We can still have implicit descendant when @nest is used even if it's optional.

vrugtehagel commented 2 years ago

This seems like a pretty important feature to get right, and having read the minutes of a recent meeting, I'm very surprised that everyone but tabatkins preferred {} over @nest. I, like tabatkins, feel very strongly that extra nesting will be quite inconvenient to read, write and maintain.

I'm actually a fan of this syntax suggested by LeaVerou earlier:

.rocket {
    --engine: super-blasting;
    color: white;
    content: "astronaut";

    @nest;
    & .engine { color: gray; }
    & .window { color: transparent; }
}

This, as mentioned above, involves a parser mode switch, which tabatkins has some concerns about, primarily these two points:

it means that the parsing context you're in is less obvious, which isn't great for authors

I'm not so sure where this is coming from. I'm going to naively assume that people will use indentation for nested rules, and so the nesting itself would be apparent from that - either way, this seems no different from SCSS, and I doubt many people struggle with what parsing context they're in when writing or reading SCSS.

It's also very awkward to manipulate with the OM

Okay, this one I understand a bit better, though (looking at the above example still) including the @nest in the OM would make no sense to me. As tabatkins mentioned before, something like

.foo {
    & .bar { color: red; }
    @nest .baz & { color: blue; }
}

would parse the nested rules

As CSSStyleRule objects in the .childRules property of the parent rule.

This makes sense. Note how the @nest is not really an at-rule in the OM. That would be weird; then either the rules are different, one being a CSSStyleRule, another being a CSSNestRule (or something along those lines), or even stranger, both would be CSSNestRules even though the & .bar does not look like an at-rule at all. So, it only makes sense to me that the earlier syntax would parse, well, identical to the way that the spec would currently parse nested selectors. It would not include the @nest; in the OM; that's just something we write to say "okay, that was all my declarations for this selector, now here comes the nested selectors". I don't see any use case for it being useful to include the @nest; in the OM.

That being said, I actually don't mind the syntax that's currently in the spec, though I'm a bit worried about people who are new to CSS. Given most rules start with & (and they do for me; I wouldn't be surprised if beginners follow this as well) it'll probably cause quite a bit of confusion and annoyance for them to try to debug why their .foo & rule didn't work, and it'll probably not be clear to them why the @nest is needed in the first place (my selector contains an &, why can't it see that?). The current spec is carefully wiggling itself through the constrains that we have, and is doing so successfully, but the wiggling part of that is visible in the syntax, and anyone that's not familiar with or interested in parsers will probably be confused and annoyed with this syntax.

I think this would be slightly reduced by asking everyone to write @nest in front of every rule, but even then; "why can't it see my selector contains an &?" is probably going to be a question floating around in people's minds quite a bit (it would for me, anyway). The syntax I mentioned before, with a @nest; delimiter between declarations and nested rules however, doesn't quite suffer from this problem as much. It's far less annoying to write it once than every rule, and a lot easier to explain to beginners why it's needed: "it's to separate the declarations and the nested rules" would satisfy me if I just started learning CSS.

Lastly, I just want to repeat that I'm absolutely wholeheartedly against more indentation (i.e. using extra {} to denote nested rules). "Just stop using 4 spaces" sounds to me like essentially the same argument as "just write @nest in front of every rule" - sure, yes, I could stop using 4 spaces, but I prefer not to because it helps me understand code. The extra indentation, as well as the towers of curly braces it comes with, look messy to me, and as tabatkins mentioned before, it'd be quite inconvenient when editing styles (specifically going from a purely nested rule to one that also has some declarations or vice versa). Especially after seeing some real-world conversions of nested styles to the {}-syntax, I would be sad to see the long awaited feature, nesting, becoming a reality in this way.

proimage commented 2 years ago

With the { ... } syntax (or the more-explicit variation that adheres to existing common CSS code patterns, @nest { ... }), the extra level of indentation could end up being a net positive for authors, as it provides a visual differentiation between properties and child selectors.

Granted, that visual differentiation is largely already there even without the extra indent (eg. in SCSS), albeit just on the nested selectors' properties, not on the selectors themselves... 🤷


To sum up, it seems like we've got four main patterns, right?

  1. & .child / @nest .grandparent & (the current spec)
  2. A @nest .selector { ... } prefix for each and every child selector
  3. Wrapping children in { ... }.
  4. A single @nest; delimiter to indicate the start of the child selectors.

It looks like we've had a grand total of 15 people chime in here. Is there a way to present the options to a somewhat wider audience of devs, without opening up another endless debate? A poll, perhaps, just to gauge general opinion among a few hundred or thousand devs instead of just us 15?


Finally, I want to triple-check on something. @tabatkins, in an interview with Chris Coyer almost exactly 10 years ago, you had this to say about considering new ideas for CSS:

Speed and implementability are important, but never top-level concerns. I generally assume that browsers will figure out how to do whatever’s necessary, and scale things down only when they push back. I’ve been surprised many times in the past both by something being easy or fast that I thought for sure would get shot down, and by things being widely panned that I was certain would be easy or popular.

Have the browser developers themselves concluded that the lookahead required to have simple SCSS-style nesting is a no-go—they pushed back on that? Apologies for not knowing if any of y'all are the browser devs of whom I speak. ;)

fantasai commented 2 years ago

+1 to @tabatkins points about problems with having certain selectors switch us into a different parsing mode. +1 to everything else @LeaVerou wrote in https://github.com/w3c/csswg-drafts/issues/4748#issuecomment-966579225 +1 to @proimage's point that we should be getting more feedback from a broader audience

css-meeting-bot commented 2 years ago

The CSS Working Group just discussed [css-nesting] Syntax suggestion.

The full IRC log of that discussion <emeyer> Topic: [css-nesting] Syntax suggestion
<emeyer> github: https://github.com/w3c/csswg-drafts/issues/4748
<emeyer> astearns: Are we ready to resolve on this? I don’t see agreement in the very large discussion.
<emeyer> TabAtkins: I’d prefer to push this off.
<fantasai> +1 to deferring
<emeyer> astearns: Tab, it would be great if you could summarize where we are and what you think we should do.
<emeyer> TabAtkins: Will do.
nileshprajapati commented 2 years ago

I have been using postcss nesting for the past month and prefer the current implementation of &{}.

body { background: black; color: white;

& main {
        background: orange;
        color: black;
}

&  p {
    font: serif;
}

}

where CSS nesting doesn't work for me; is the modifier (BEM methodology ) using the double dashes like the example below:

card { &--large { // } }

brechtDR commented 2 years ago

Personal, I like the ampersand implementation. Because it feels more natural for people coming from sass. Although, I'm a bit scared when it comes to consistency. Which might make me prefer to write @nest all the time. On the other hand you could write company/internal guidelines to always use @nest.

Why i'm not a fan of extra brackets and indentation, Is because of how wide your file potentially could become. I agree that you could write guidelines about it, but people are people and deep nesting will happen, with or without guidelines. Companies already make guidelines such as "only nest 3 levels deep in sass", and still sometimes people go deeper. But even with just 3 levels you would already create at least 6 levels of indentation. It feels more unnatural, and more likely will get picked up a lot slower just because of that.

sandwich commented 2 years ago

Hey y'all, I just saw the poll. Did I miss something? Why is the brackets syntax shown as requiring ampersands somewhere in the selector of every nested rule? Isn't that redundant in the cases I've highlighted below?

/* Example 1 */

.foo {
  color: #111;

  {
    & .bar { /* <----  Unnecessary? */
      color: #eee;
    }
  }
}

/* Example 3 */
.foo, .bar {
  color: blue;

  {
    & + .baz, /* <----  Unnecessary? */
    &.qux {
      color: red;
    }
  }
}

/* Example 4 */
figure {
  margin: 0;

  {
    & > figcaption { /* <----  Unnecessary? */
      background: lightgray;

      {
        & > p { /* <----  Unnecessary? */
          font-size: .9rem;
        }
      }
    }
  }
}

/* Example 9 */
dialog {
  border: none;

  {
    &::backdrop {
      backdrop-filter: blur(25px);
    }

    & > form { /* <----  Unnecessary? */
      display: grid;

      {
        & > :is(header, footer) { /* <----  Unnecessary? */
          align-items: flex-start;
        }
      }
    }
  }

  {
    html:has(&[open]) {
      overflow: hidden;
    }
  }
}

Doing this basically entirely negates the whole purpose of having brackets (or parentheses) enclose multiple nested selectors in the first place. Did I miss something? 😕

mirisuzanne commented 2 years ago

@sandwich I don't think you missed anything - likely I did when reviewing that article. I did not think of that as 'the whole purpose of having brackets', and so it didn't bother me that they were included sometimes when not absolutely necessary. To me, the purpose of brackets was to remove any need for an additional syntax when the & is not at the start of the selector.

proimage commented 2 years ago

@mirisuzanne Ahh, I think I understand you, and if so, that's a pretty major difference. Lemme get this straight... you envisioned that brackets would only really be a benefit in this situation?

.foo {
  @nest .bar & { color: red; }
  @nest .baz & { color: green; }
  @nest .blah & { color: yellow; }
}

i.e. it could be:

.foo {
  {
    .bar & { color: red; }
    .baz & { color: green; }
    .blah & { color: yellow; }
  }
}

?

Sure, I guess that's one of the advantages. But on my end, I was thinking that the brackets would be most useful in situations where instead of repetitive ampersands before each nested selector:

.foo {
  & .bar { color: red; }
  & .baz { color: green; }
  & .blah { color: yellow; }
  /* etc */
}

...you'd just have a clean look like this:

.foo {
  {
    .bar { color: red; }
    .baz { color: green; }
    .blah { color: yellow; }
    /* etc */
  }
}

I'm trying to think practically here. Lots of devs, myself included, would be coming from SCSS. The syntax I was intending was to help minimize work needed to convert existing SCSS; just select nested selectors and wrap with brackets—done. After all, I'd guess that something like 90%+ of the time, we only use simple nesting (.foo { .bar { ... } }) instead of the more complex .foo { html.fooActive & { ... } } type stuff...

sandwich commented 2 years ago

I'll take your thumbs-up of my post as your response. ;) So, with that in mind... is the poll a legitimate representation of the brackets syntax?

I'd postulate that it's not, and therefore any poll result isn't a fair representation of the opinions of the participants. So.... how do we best handle that?

EDIT: Sorry for mixing up my two accounts. At least the avatar's the same? 😬

mirisuzanne commented 2 years ago

There are any number of improvements that could be made: additional choices, more real-world examples, ranked choice voting, random sampling, etc. But this was always only a rough straw poll, not an official vote to determine the syntax. It did what was intended, by starting a conversation that has already given us a lot of feedback in a number of different forms - both in the survey and around it (like this).

To best handle it from here, we make sure to take all of that feedback (including the flaws of the poll itself) into account as we continue to discuss what syntax we'll use.

proimage commented 2 years ago

Oki-doke. :) Thanks!

LeaVerou commented 2 years ago

I feel @proimage/@sandwich's comments are yet another data point that descendant selectors should be the "default" nesting, and that applies to every syntax (brackets, @nest etc). E.g. @nest & .foo feels very redundant.

Loirooriol commented 2 years ago

https://github.com/w3c/csswg-drafts/issues/5738#issuecomment-736750108 explains why it wouldn't be a good idea to accept selectors without & and just assume it should be inserted at the beginning with a descendant combinator.

vrubleg commented 2 years ago

@Loirooriol Thanks for this link. I guess it is also a good reason to not allow implicit & in the third syntax variant with inner { } block for nesting.

proimage commented 2 years ago

The only thought I have left is to see what each syntax would look like in a real world scenario. These by-necessity minimal code samples here and on the poll page are one thing, but what would the code look like, from a maintainability and readability perspective, if it were, say, fully styling a main menu with drop-downs? I'm thinking, something with at least 100 lines of code, where most selectors have at least 5-10 properties.

If I have time, I'll try to post something like that. Would doing it in a Gist be better than code blocks in here?

proimage commented 2 years ago

Hmm, I have what I thought was a great code sample... a 135-line main menu SCSS. However, it's fairly BEM-y (ABEM, to be precise), which I believe is the one syntax that native nesting—regardless of the syntax—kinda sucks at. Here it is for now, but I'll probably rework it to remove the BEM-iness. I've reworked it to be a more-traditional, multi-layered, nested SCSS (you can find the old ABEM version in the revisions page). CSS Nesting syntaxes coming soon!

https://gist.github.com/proimage/6a74e303265661531c7c72545dba4068

proimage commented 2 years ago

I've finished with the syntax conversions (I wish there was a way to re-order the files in a Gist): https://gist.github.com/proimage/6a74e303265661531c7c72545dba4068

I have some thoughts about the process I just went through generating these nesting syntax variations, but I need to contextualize them first. These thoughts are related to the process of converting existing SCSS over to the three polled formats. They may or may not be applicable to the process of writing these syntaxes from scratch.

Thoughts on the SCSS -> @nest syntax conversion:

Thoughts on the SCSS -> @nest syntax conversion:

Thoughts on the SCSS -> { } brackets syntax conversion:


With all that said, I think the ideal solution from my own, developer POV (i.e. I'm not a parsing engine coder) would be an alternative of the @nest syntax, where the & char is used to define a "modifier" selector of the parent, but (here's the difference) where a different character would be used to indicate a child element selector. Something like this:

.parent {
  font-size: 18px;
  color: blue;
  @nest html.hideParent & {
    display: none;
  }
  &.modifier {
    font-weight: bold;
  }
  % .child {
    display: flex;
  }
}

I don't know if that's possible or not, but it would basically solve the one remaining issue I have with the @nest syntax (differentiation between modifier and child selectors). 🤷‍♂️

argyleink commented 2 years ago

we should be getting more feedback from a broader audience

✅ Poll has run long enough to share the results:

![image](https://user-images.githubusercontent.com/1134620/184020303-d5f52733-8725-40c2-abdd-7f687b219311.png)
6,661 votes for @nest, 338 votes for @nest always, 541 votes for brackets



Should we add this to the next agenda for discussion?

davidwebca commented 2 years ago

Where was this poll even posted. Rip, completely missed it... I'm not sure it reached the audience 😬

vrubleg commented 2 years ago

It was posted here: https://developer.chrome.com/blog/help-css-nesting/

chee commented 2 years ago

i prefer @nest always because it means i don't have to learn when to use @nest

bradkemper commented 2 years ago

I prefer the combination of @nest and ampersand. To me it feels like the easiest transition from using SCSS. I don’t mind using & .child (with the space in between), because I am already used to doing things like & .child1, & .child2 in SCSS. I honestly don’t know if that is necessary or not in SCSS when using selector lists, but it always feels safer and more clear. Using @nest Only would be be too cluttered IMO. I’ve tried to warm to the idea of using bare curly braces, but I think it would be harder to get used to, and more prone to error: I think it would be harder to spot the places where I left them out accidentally.

scherii commented 2 years ago

@chee If I understood everything correctly, all you need to know about @nest (first variant) is that the notation known from SCSS comes into play, except when the nested element is a parent of it (e.g. styling an element based on if a dialog is open: html:has(dialog[open])).

So there is not much to learn.

Whereas @nest always feels extremely verbose to me, and depending on the codebase, could blow everything up quite a bit. brackets, on the other hand, increases indentation by one compared to the other two.

TL;DR:

  1. @nest = minmal, concise, with the possibility of using @nest for parent selectors
  2. @nest always/restricted = verbose, tends to bloat the codebase
  3. brackets = increases indentation by 1
LeaVerou commented 2 years ago

Since this is on the agenda for today and I can't join, I'll post my thoughts here:

While the current spec is not ideal (authors basically want Sass-style nesting), it's far, far better than any of the other solutions posted here. @nest all the time would make stylesheets tedious to write and noisy to read, and the brackets would make them highly nested, and would need horizontal scrolling to read.

I have been using the current syntax very extensively through PostCSS and it works well. I very rarely need to use @nest at all, in nearly all cases it's just &. I'd propose we stop trying to fix something that isn't broken, and just close this issue. We've explored the design space sufficiently, and there doesn't seem to be nothing better that satisfies the parsing constraints.

proimage commented 2 years ago

I concur, but I think it would be nice to have this close with a summary of the agenda discussion, so I won't close it quite yet.

Thank you, all, for a very thorough, 2.5 year (😲) look at this issue!

mirisuzanne commented 2 years ago

I agree, in practice the spec syntax works best, despite the apparent complexities. But at this point we actually need to revert the previous resolution - where we voted to use brackets instead. As one of the people behind that resolution, I'm now fully in support of reverting it, and implementing the spec as originally written.

LeaVerou commented 2 years ago

I agree, in practice the spec syntax works best, despite the apparent complexities. But at this point we actually need to revert the previous resolution - where we voted to use brackets instead. As one of the people behind that resolution, I'm now fully in support of reverting it, and implementing the spec as originally written.

Yikes, yes. How did that happen?

css-meeting-bot commented 2 years ago

The CSS Working Group just discussed Nesting syntax, and agreed to the following:

The full IRC log of that discussion <TabAtkins> Topic: Nesting syntax
<TabAtkins> github: https://github.com/w3c/csswg-drafts/issues/4748
<fantasai> ScribeNick: fantasai
<fantasai> TabAtkins: Some time ago we had a discussion about what style of nesting syntax should use
<fantasai> TabAtkins: major options are what I drafted, nesting selector directly with an &
<fantasai> TabAtkins: or use @nest if needed
<fantasai> TabAtkins: option 2 is @nest always
<fantasai> TabAtkins: and option 3 is using brackets to wrap nested rules, never use @nest
<fantasai> TabAtkins: At the time we did a straw poll, and WG resolved on using brackets
<fantasai> TabAtkins: I was unhappy with this and we resolved to take it back for more data collection or arguments
<fantasai> TabAtkins: Adam Argyle ran a poll with significant response numbers about this
<TabAtkins> https://developer.chrome.com/blog/help-css-nesting/
<TabAtkins> https://github.com/w3c/csswg-drafts/issues/4748#issuecomment-1211280306
<fantasai> TabAtkins: Linked article, I think was written fairly
<fantasai> TabAtkins: It seems responses are incredibly one-sided
<fantasai> TabAtkins: Using & or @nest directly
<fantasai> TabAtkins: This is what's in the current spec and what I prefer
<fantasai> TabAtkins: This was an overwhelming response in favor of one option
<fantasai> TabAtkins: So suggestion is to revert previous resolution and keep syntax in current spec
<astearns> option 1 also allows you to write as if @nest is always required
<Rossen_> ack fantasai
<miriam> q+
<TabAtkins> fantasai: The problem I noticed reading thru the thread is the bracketed syntax was represented as always putting in the ampersand, while the point of bracketed syntax was to avoid needing it
<TabAtkins> fantasai: So I dont' think that's true that it was fairly written
<TabAtkins> TabAtkins: I didn't take that as part of the syntax at all, still needed the & to be the nesting selector
<TabAtkins> fantasai: The point was that it was mostly never needed, so I think the poll wasn't valid
<TabAtkins> miriam: I reviewed the article and helped come up with the syntax, so...
<Rossen_> ack miriam
<TabAtkins> miriam: I didn't think of removing the & as the main advantage of the brackets
<TabAtkins> miriam: As I was writing it I foudn the brackets more confusing than expected, and I actually *added* ampersands for clarity
<TabAtkins> miriam: AFter writing the demos I actually changed my mind away from bracketing
<TabAtkins> fantasai: I'm not going to block, if y'all liek this syntax better that's fine. I just don't like having an inconsistent syntax.
<TabAtkins> fantasai: I just don't think it's fair to say it was overwhelmingingly in one direction
<TabAtkins> Rossen_: So it sounds like we have a pretty strong response. Possibly biased, but Mia makes an argument for it not being so.
<fantasai> I really don't like the inconsistency, where you have to know when to use @nest
<Rossen_> q?
<TabAtkins> Rossen_: So proposed resolution is to undo the resolutions from november and leave the spec as it currentyl is
<TabAtkins> Rossen_: Any additional comments or feedback?
<TabAtkins> fantasai: I'm not gonna object because everyone seems to like it, but I really don't like that authors have to know when to use @nest, it's not a consistent syntax
<TabAtkins> fantasai: And I wish we had something else besides an inconsistent syntax
<TabAtkins> fantasai: I don't like @nest everywhere since it's verbose, but...
<TabAtkins> miriam: I agree on the problem and it's why I liked bracket before. I agree it's an issue and it's weird
<TabAtkins> miriam: But once I started writing examples, it almost all basic use-cases you just use the & and it works and looks cleaner.
<TabAtkins> miriam: and there are only rare cases where you need to use @nest and it's not as hard as I initially thought to know the difference
<TabAtkins> miriam: So that's why I changed my mind even tho I agree it's inconsistent
<bradk> +1 to miriam
<TabAtkins> plinss: I think elika has a valid point and before we decide on this we could make another design phase to try to come up with another route.
<fantasai> I'm not so opposed to the & , just that some rules require @nest and others dont'
<TabAtkins> plinss: I'm okay with undoing the previous resolution, just not sure we shoudl resolve affirmatively on the current syntax
<TabAtkins> Rossen_: Right, so let's just resolve on undoing the previous resolution.
<TabAtkins> Rossen_: Objections?
<TabAtkins> RESOLVED: Revert the previous resolution from Nov 2021 mandating bracket-nesting syntax, and the WG preference for a single nesting syntax.
<TabAtkins> Rossen_: And for the record, want to strongy encourage continued improvement of the design.
romainmenke commented 2 years ago

Didn't see it come up in the minutes but I think the results of any poll directed at stylesheet authors will be biased when it comes to nesting.

In this case they were given the choice between the syntax that they use today with postcss-nesting or two alternatives.

They will always pick the one that they use today.

If they were given the choice for sass-like nesting they would have picked that because it is still the mental model for nesting for the majority of authors.

This is the downside of pushing out polyfills for features that aren't stable even in spec form. In postcss-preset-env we are moving much more slowly now but we can't take back nesting :/


meaningless extra data points :

It seems that there were some question about & vs @nest initially but mostly people seemed to find their way even without a single syntax for nesting.