w3c / csswg-drafts

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

[css-nesting-1] Syntax Invites Errors #7834

Closed fantasai closed 1 year ago

fantasai commented 1 year ago

Edit: 👉🏼 UPDATED SUMMARY TABLE OF SYNTAX PROPOSALS 👈🏼 (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:

bramus commented 1 year ago

Also not a fan of a parsing switch.

This would require the author to keep track of which mode they are in: by looking at a nested selector they cannot know if they already are in one of both modes or not.

.foo {

  /* insert 20 declarations here */

  .bar & {}  /* 🤔 Wait, which mode am I in right now? */
}

It also can become confusing, as moving blocks of code – e.g. a nested at-rule – can affect other pieces of unrelated code below it.

.foo {

  /* insert 20 declarations here */

  @media(…) {
    …
  }

  .bar {}  /* ❌ This line will suddenly break when you move/remove the nested at-rule included above it */
}
.foo {

  …

  & .foo {
    …
  }

  .bar {}  /* ❌ Moving this block a few lines upwards when reordering your code, requires the addition of an extra leading & in order to keep things working */
}

These last ones remind me of the “Two CSS properties walk into a bar” CSS joke, in a very negative way.


(@tabatkins) 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.

Seems like a pretty significant limitation to introduce.

vrugtehagel commented 1 year ago

I've been very confused, like romainmenke, about some steps that were taken. The Syntax suggestion issue was promptly closed with a simple comment

Closing as resolved.

However it looks to be a completely open-ended thread and it is not at all clear how this resolution was reached, and more importantly, what the resolution actually was. And, when looking at this new thread, it doesn't seem like the syntax issue has been resolved even a little bit - it seems like we just somehow marked the previous issue as "resolved" just to continue the conversation here.

It would be great to get some clarification as to what happened so that people like myself can feel like they are up-to-date.


In the previous issue, I advocated for a syntax that simply required @nest; before using nested rules. There were no additional requirements. In my opinion, it is absolutely vital that syntax is clear and consistent. The suggestions seem to be "well, you can just start nesting, unless your rule starts with a character that is not part of &, or @ (some suggestions including all non-alphanumerical characters), then you either need to switch the order of the rules so that it does, or use an @nest;". There are some JavaScript APIs that are in a similar manner dependent on context; e.g. document.all('foo') could return an element with id="foo", but also an element with name="foo", and actually if there are multiple elements with name="foo" on the page, it returns an HTMLCollection with all of them. Of course, this is CSS, and so it is very different in nature than JavaScript, but I can't help noticing the similarities (and I'm sure most of us agree that the API design of document.all was not good). I would think it's much safer, usable and future-proof if we go with something much more simple, like the required @nest;.

Additionally, I think we're missing the point by trying to stay as close as possible to SCSS syntax. The way we're trying to do that is inevitably going to lead to situations where people expect a piece of SCSS to just work, but it doesn't. Minimizing the additional effort to convert a piece of SCSS to CSS is a valiant effort however but it does not mean we have to mimic SCSS's syntax. With the required @nest;, authors would be aware this is necessary for their CSS to be valid. They can copy-paste SCSS, they will add the @nest; if necessary, and there's no additional overhead of "oh, should I be changing stuff here? Do the rules start with the right characters, or should I change their order?".

Let me also bring up a previous point; the context awareness. Some people have mentioned that authors should be able to just see what context they are in (which is important if we introduce a parser switch). Now, overall, I don't think the parser mode will be difficult to recognize (and people won't even really need to know about the parser switch or what that even means), because a rule is a rule and a declaration is just that. We've already been doing it in SCSS and other preprocessors for a long time, proving this is not hard for authors to get used to. However, with the current discussion and suggestions in mind, if I see

    .red { color: red; }

and change it to

    h1 { color: black; }
    .red { color: red; }

is this valid? Uh, well, that depends; what's above it? Conversely, if I see .red { color: red; }, can I just remove that? Well, that also depends; if it has a rule in front of it, then yes, but if it's the first rule, then then next rule should... Etcetera. In terms of syntax it's a nightmare of mental overhead.

That seems like quite a problem. With something trivial like a required @nest;, the answers to the questions are obvious. You need a @nest;, so the simple addition of a rule like above is allowed regardless of context. There is a rule already, therefore there is an @nest;, which means that yes, you're allowed to add a rule. If you see a rule like .red { color: red; }, sure, remove it. Your CSS is guaranteed to still be valid.

Lastly, I'm going to repeat a point I mentioned in the previous thread. I mentioned then 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.

and I think we're merely amplifying this by throwing even more "if this, then..., otherwise..." in the mix.

To avoid confusion, I do not think a required @nest; is "the" solution, but in my opinion, it ticks the most boxes of any suggestion so far. Specifically the simplicity and consistency that I believe is so important in language syntax is missing from many of the suggestions made in this thread.

jimmyfrasche commented 1 year ago

I think @nest-everywhere but with the @nest auto-insertion rules so you rarely have to actually write it would work well in practice without the sharp edge of the mode-switch

LeaVerou commented 1 year ago

@bramus The current evolution of the proposal would kick into rule parsing mode with . as well, so nothing would break in your code examples if you move the @-rule.

@devongovett

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.

@romainmenke

My push back is because you can omit it in subsequent selectors, not because it is required in the first selectors.

The primary feedback we get from authors over and over is that they dislike that the & is mandatory with the current syntax, and want a syntax that just assumes descendant if no & is present. We are not trying to design a syntax closer to Sass because we believe that migration matters that much (as you point out it's a one time cost), but because if there were no parsing limitations, it appears that the Sass syntax has the best ergonomics.

If some authors think the & should be mandatory always, in every rule, and some authors think it should be completely optional, I'm not sure how we will reach consensus, except by majority, which is not a good way to make decisions 😕

Though I will point out that if the & is optional, nothing prevents you from having style guides that mandate it, so you can still write and read code the way you find preferable in your team, so in theory everyone can be happy. Making it mandatory means the % of authors that consider it noise to read and tedious to type cannot be happy.

devongovett commented 1 year ago

I would just challenge whether that feedback is because people are familiar with SASS, or because it's intrinsically better/clearer. Disliking syntax is also different from not understanding it. People disliked the custom property syntax in the beginning as well.

fantasai commented 1 year ago

There are a lot of comments about how “converting from XX framework is not a big problem”. Yeah, I agree; it's a one-time cost, you can run a script. That was not the point about conversion I opened this issue for; rather, it is specifically about copying code from one context to another in pure CSS. People write CSS within nested contexts and outside it, and within our future @scope rules and outside it, and it is quite reasonable and common to copy code from one place to another. Copying code should work with minimal friction. Switching a block from nesting to scoping should work without friction. That's the issue.

Making & valid outside of nesting is great, we should do it, that's a separate issue, and it doesn't solve the problem that most unnested CSS code is written without it today and will likely continue to be written without it in the future.

As for the performance concerns of descendant selectors: authors want to use descendant selectors, and they will. I expect whether we require & here or not will hardly make a difference.

In the future, some large percentage of CSS rules will be nested. Whatever we do here, it needs to be ergonomic enough to be used for the majority of style rules in a website. And it needs to work naturally with @scope.

romainmenke commented 1 year ago

Can we split up this proposal in multiple threads? Too much is being lost because everyone focuses on one ore two details of the previous statement. It is not easy to discuss the core of this proposal without getting stuck discussing the side effects.

I see these parts, but maybe there are more:

descendant operator as an implicit default when & is missing is not in this list because it is a side effect of having relative selector syntax and a parser switch mechanic.

Things like the parser switch mechanic need to be researched in detail because it is something we can not take back, it will change the language fundamentally. A separate thread is more suitable in my opinion.

bradkemper commented 1 year ago

.bar & {} / 🤔 Wait, which mode am I in right now? /

To any CSS author that is clearly a rule, not a declaration, so I don't think that is confusing at all. We're saying now that non-idents, such a ., #, >, +, :, &, etc. would signify the beginning of a rule.

I agree with @LeaVerou and @fantasai that the reason we keep mentioning SASS (SCSS dialect) is not just author familiarity, but because it has good writing ergonomics, which complement how people author CSS. It makes it really simple to write nested descendant rules and rules that start with other combinators, and the & character is easy to use for when you want to modify the subject selector in other ways (e.g. add a class to it, or prefix some other selectors to it). We should be aiming for something as close to this as we can, without boggling the parser or hurting parsing performance. Making everyone type a & or @nest before every selector is excessive, if we can avoid it. And we can.

I actually don't hate @LeaVerou's "crazy" idea. I could see that working. But I'd still rather just have that as the restriction on the first rule only, which is where we pretty much are at this point anyway. But it is a good framing: to disambiguate the beginning of nested rules from the declarations (which is the main reason to diverge from SCSS nesting at all), the first rule of the nest just can't start with a tag name selector. But it can start with a &, and every other kind of selector is fine. That isn't an unreasonable thing to remember, even when moving rules around or deleting them. Kind of like how you can omit the last semicolon of a rule, but if you move that rule or add one after it, you'll need to remember to put that semicolon in there after all. In this case, you'll need to remember that the first rule of the nest has to start with something other than a tag name.

And if it helps your individual authoring style, precede all your tag-name-starting nested rules with a &. For the minority of rules that need the & elsewhere in the selector, and you don't have any other rules to put before it, I personally think preceding that selector (or the whole rule) with @nest is a little easier to read that wrapping the tag name with :is() but maybe that's just me. I could get used to either way.

bramus commented 1 year ago

.bar & {} / 🤔 Wait, which mode am I in right now? /

To any CSS author that is clearly a rule, not a declaration, so I don't think that is confusing at all. We're saying now that non-idents, such a ., #, >, +, :, &, etc. would signify the beginning of a rule.

Thanks for clarifying (both you and @LeaVerou above).

tabatkins commented 1 year ago

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.

Seems like a pretty significant limitation to introduce.

It's probably not, I think. For one, we don't actually have any plans to do anything like that, so any concerns would be purely theoretical; this isn't a syntax space we've ever played in or planned to. For two, for the same reason I have to be careful in writing the syntax spec algo (to avoid accidentally switching into "rule mode" when someone just used an ascii char to "comment out" a property), we'd have to be extremely careful to avoid accidentally turning currently-invalid code into correct code with an unintended meaning. More than likely we'd just avoid this altogether and stick with putting new syntax between the property name and the colon instead, which is also compatible with this Nesting syntax change.

devongovett commented 1 year ago
div {
  background: green;
  .background: red;
  color: white;
}

With code like the above, the color property still applies due to error recovery. But if . started a different parsing mode, it wouldn't. Same with & or any other currently-invalid character. It may be particularly common with * as that was historically used as an IE-specific hack, but would also conflict with a selector.

tabatkins commented 1 year ago

Fortunately that's incorrect, due to how I proposed changing Syntax. Instead, that . would cause us to attempt to "parse an ambiguous rule", and when we hit the ; we'd realize it was instead an invalid property all along and just drop it, without changing the parsing mode. The color property would then continue to be valid.

tabatkins commented 1 year ago

To organize the discussion a bit for tomorrow, the options we're looking at are:

  1. Current spec - every nested rule needs to be unambiguous on its own, either by starting with an & or by being an @nest rule. If not using @nest, every selector in a list needs to start with &, not just the first.
  2. Parser switch proposal - after some parsing switch has been tripped, everything's assumed to be a nested rule. There are a few possibilities for the parsing switch:
    1. Just at-rules. This means any nested at-rule, like a nested @media, or the no-op @nest; rule we'd introduce.
    2. (link) The above, plus any style rule starting with an &. (Rules following the switch can start with whatever.)
    3. (link) The above, plus any style rule starting with a non-ident. (So .foo, :hover, etc will trigger the switch, but div won't.) (Rules following the switch can start with whatever.)
  3. Lea's proposal - No parsing switch, instead every nested rule has to be unambiguous on its own, by starting with anything but an ident. (You can write & div or :is(div) if you need to start a selector with a type selector.) (This employs the same parsing strat as (2.3) to avoid accidentally parsing invalid properties like //color: red; as rules.)

Arguments for each of the above options:

#ProsCons
(1)
  • Every rule is valid or invalid "locally", no need to track context.
  • `&` or `@nest` is visually distinct from properties.
  • `@nest`, if used only when needed, signals "odd" nesting. (But might be used anywhere.)
  • Theoretically can mix properties and rules in any order, tho we won't retain their relative order in the data model. (All properties will be treated as preceding all rules.) (Currently the spec disallows this, to avoid confusion.)
  • Syntax is different from other nesting contexts (like `@scope`, or global `@media`), so you can't copy from `@scope`/etc to nesting. (It might be safe to copy from nesting to `@scope`/etc, if we explicitly allow `&` and `@nest` globally; see #5745.)
  • Requiring each selector in a list to be modified with & is error-prone (easy to forget) and is complicated to convert manually or automatically
  • More verbose than Sass/etc-style, which many authors are used to. (And is arguably just a good design.)
(2.1)
  • After the switch, syntax is the same as other nesting contexts.
  • Syntax is same as Sass/etc-style, which many authors are used to. (And is arguably just a good design.)
  • The `@nest;` no-op rule is weird and requiring it everywhere is very noisy.
  • Can't *quite* naively move code between nested contexts; need to make sure the switch is there (or add it) when moving *to* plain nesting. (But moving to other contexts is always safe, even if you copy over the `@nest;` too.)
  • Can't mix properties and rules - all properties have to come first. (But this matches the data model anyway.)
(2.2)
  • Same as (2.1), but you can avoid using `@nest;` most of the time if you instead start your first rule with `&`.
  • Need to pay somewhat more attention to context, and make sure your first rule is written correctly - either preceded by an at-rule, or starting with `&`.
(2.3)
  • Same as (2.2), but you can avoid using `@nest;` in even more cases: unless your first selector starts with a type selector, you can just nest naively.
  • Still somewhat context-sensitive, just less so than (2.2).
  • Prevents us from ever changing property syntax to start with an ascii glyph. (Like `+transform:...;` for additive properties?) (But these are probably already ruled out anyway, due to people using garbage to "comment out" their properties, like `//color: red;`, or `*color:red;` for an old IE hack.)
(3)
  • Like (1), every rule is valid or invalid "locally", no need to track context.
  • Like the (2.X) set, can *mostly* transfer rules between nested contexts. Going *from* nested to `@scope`/etc is always valid; going from `@scope`/etc *to* nested is *usually* valid, unless the rule starts with a type selector.
  • Like the (2.X) set, syntax is same as Sass/etc-style except for selectors starting with a type selector.
  • Like (1), can theoretically mix properties and rules again, but the data model will still have to act as if all properties as coming first.
  • No `@nest` rule needed
  • Rules are invalid if they start with a type selector, requiring them to be rephrased somehow. (Using `:is(div)`, starting with `&`, etc.)
  • Like (2.3), prevents us from changing property syntax to start with an ascii glyph in the future. (But similarly, this is probably already lost to us.)
devongovett commented 1 year ago

Instead, that . would cause us to attempt to "parse an ambiguous rule", and when we hit the ; we'd realize it was instead an invalid property all along and just drop it, without changing the parsing mode.

Ah, I missed this detail, thanks. This does have the downside that a semicolon after a nested rule means that rule is thrown away. Could trip people up.

.foo {
  .bar { color: red }; /* not applied. remove the semicolon and it works! */
}
tabatkins commented 1 year ago

Nope, that's fine too, because the } will cause the algo to return the rule successfully (and trip the parsing switch). It won't continue looking forward after it's found a complete rule.

The ; will then screw with the next rule, but that's true of any garbage following a rule. In your precise example it does nothing since there's no following rule.

romainmenke commented 1 year ago

Syntax is same as Sass/etc-style, which many authors are used to. (And is arguably just a good design.)

Can we be specific about the Sass comparisons? This proposal would only make it more similar to Sass styles nesting in one very specific aspect .foo { .bar {} }.

It will not allow BEM style selector concatenation and it will not match the same elements when complex selectors are involved and & is not part of the first compound selector.


2.x : Can we list "requires a parser switch mechanic" as a Con?


2.3 and 3 :

Prevents us from ever changing property syntax to start with an ascii glyph. (Like +transform:...; for additive properties?) (But these are probably already ruled out anyway, due to people using garbage to "comment out" their properties, like //color: red;, or *color:red; for an old IE hack.)

It also limits future combinators and selector syntax because the ambiguity goes both ways.

Some examples which haven't come up yet. These might be fine, but they illustrate the point.

.foo {
  color: green;

  * {
    color: purple;
  }
}

.foo {
  color: green;

  ||td {
    color: purple;
  }
}
tabatkins commented 1 year ago

It will not allow BEM style selector concatenation

Right, BEM-style concatenation was always a mistake and will never be part of CSS. However, anyone not using BEM or similar (or using a preprocessor with a similar syntax that doesn't have that feature) will be fine.

and it will not match the same elements when complex selectors are involved and & is not part of the first compound selector.

I don't understand what you mean by this. .foo { .bar & {...} } will match the same elements in both contexts (equivalent to .bar .foo).

It also limits future combinators and selector syntax because the ambiguity goes both ways.

I'm not sure I understand what you mean. We can continue to innovate in any way that doesn't make properties start with a non-ident (already probably necessary) or rules start with idents. All your examples, and all similar ones with new selectors or combinators, will be fine in the future.

romainmenke commented 1 year ago

I don't understand what you mean by this. .foo { .bar & {...} } will match the same elements in both contexts (equivalent to .bar .foo).

.foo .bar {
  @nest .other & {
    color:green;
  }
}

css: .other :is(.foo .bar) sass: .other .foo .bar

But these are probably already ruled out anyway, due to people using garbage to "comment out" their properties, like //color: red;, or *color:red; for an old IE hack

I'm not sure I understand what you mean. We can continue to innovate in any way that doesn't make properties start with a non-ident (already probably necessary) or rules start with idents. All your examples, and all similar ones with new selectors or combinators, will be fine in the future.

I am maybe misunderstanding the first quoted statement. So maybe it is fine if * was used for some IE hack.

tabatkins commented 1 year ago

[example]

Ah, yeah, sure.

I am maybe misunderstanding the first quoted statement. So maybe it is fine if * was used for some IE hack.

Yes, it's fine if you write the parser change carefully, which I did. ^_^

FremyCompany commented 1 year ago

I am not a huge fan of the proposed trick to change the parsing mode. I find it extremely confusing. In addition to that, it will be a pain for all libraries that need to parse CSS.

I have been thinking about this since I saw the proposal, but didn't have a concrete alternative to propose, but now I do.

How about we instead allow "post-nesting"?

article {
    color: gray;
}
@nest {
    h1 { color: black; font-weight: bold; }
    h2 { color: black; }
}

or

article {
    color: gray;
} & {
    h1 { color: black; font-weight: bold; }
    h2 { color: black; }
}

Pros:

  1. this is gramatically clean, blocks either contain declarations or rules, not both
  2. this does not cause a double-nested indentation
  3. this does not require using & everywhere for selectors that do not require it

Cons:

  1. this requires another pair of brackets
  2. this comes at the cost that "nesting" is a bit more implied than obvious
Myndex commented 1 year ago

I am not a huge fan of the proposed trick to change the parsing mode. I find it extremely confusing.

I've been trying to wrap my head around some of the proposed draft nesting rules, finding them a little confusing as well. I never use SASS, so personally do not benefit from incorporating any SASS syntax.

On the other hand @FremyCompany's approach:

article {
    color: gray;
} & {
    h1 { color: black; font-weight: bold; }
    h2 { color: black; }
}

Is simple, clear, intuitive. Keeping the nesting & outside of brackets alleviates the ambiguous "is it a property or selector" parsing issue.

Only, how does it work for multiple nest levels? And split nests? Also, I'm concerned that closing a selector's bracket before nesting is counter to how other languages nest things such as if or for statements.

So, following this idea to use ampersand to define a nest group instead of in front of every nested selector, would it make more sense to use a different bracket type for enclosing nested selectors, either singly or as a group, which also allows not closing a selector before nesting as in the above example?

Example

Here using &[] to define nest groups:

section {
  margin: 0;
  padding: 0.5em;
  & [
    article, aside {
      color: #010a12;
      background-color: #e6e0dd;
      & [
        h1 { color: #444; font-weight: bold; }
        h2 { color: #333; }
        .myClass { font-family: Barlow, sans-serif;}
      ]
    }
    aside {
      background-color: #cde;
      & [ p {font-size: 0.95em;} ]
    }
  ]
}

So here, nests are fully enclosed in square brackets (though perhaps () or <> might be more appropriate?) with I believe the same advantages as @FremyCompany's idea, though making multiple nest levels and nesting hierarchy more clear.

split nests and regex captures

So, relative to the above example:

    article, aside {
      & [ p {font-size: 0.95em;} ]
    }

Is essentially the same as

    article p, aside p {font-size: 0.95em;} 

Instead of an ampersand, what if we used a $ as the symbol for the parent selector(s)? In this usage, $ or $0 would mean all of the listed parent selectors, and $1 to $9 would be each parent selector in the list, in order.

    article, aside, span {
      $ [ p {font-size: 0.95em;} ]
      $1,$2 [ p { color: red;} ]
      $3 [ p {color: green;} ]
    }

So this would be functionally the same as:

    article p, aside p, span p { font-size: 0.95em;}
    article p, aside p { color: red;}
    span p { color: green;}
    }

The advantage here is more granular control over what parent selectors we are nesting under.

Thank you for reading

tabatkins commented 1 year ago

How about we instead allow "post-nesting"?

Non-nested nesting means we can't nest two levels deep in an unambiguous way.

So, following this idea to use ampersand to define a nest group

The group has already rejected "extra level of nesting" proposals - it polled very badly with authors.

Myndex commented 1 year ago

Hi @tabatkins

Non-nested nesting means we can't nest two levels deep in an unambiguous way.

_The group has already rejected "extra level of nesting" proposals - it polled very badly with authors._

I'm not sure I understand: statement one suggests the desire to nest two levels deep, but statement two say no extra levels of nesting...

??

And well before I saw this, I opened a new issue #7877 which is a much more complete discussion and more thought-out method/syntax than what I posted here. It's not really about "extra level" it is about the benefit of grouping, and supports unambiguous levels, and specific selection from a list of parent selectors.

FremyCompany commented 1 year ago

How about we instead allow "post-nesting"?

Non-nested nesting means we can't nest two levels deep in an unambiguous way.

I don't see why?

article {
    color: gray;
}
@nest {
    h1 { color: black; font-weight: bold; }
    @nest {
        b { font-weight: 900; }
    }
    h2 { color: black; }
}

===

article { color: gray; }
article h1 { color: black; font-weight: bold; }
article h1 b { font-weight: 900; }
article h2 { color: black; }
Loirooriol commented 1 year ago

Non-nested nesting means we can't nest two levels deep in an unambiguous way.

@tabatkins Why? It seems to me that

article {
  color: gray;
  @nest;
  h1 {
    color: black;
    font-weight: bold;
    @nest;
    span { color: pink }
    b { color: orange }
  }
  h2 {
    color: black;
    @nest;
    span { color: cyan }
    b { color: yellow }
  }
}

could be

article {
  color: gray;
}
@nest {
  h1 { color: black; font-weight: bold; }
  @nest {
    span { color: pink }
    b { color: orange }
  }
  h2 { color: black; }
  @nest {
    span { color: cyan }
    b { color: yellow }
  }
}

It would be somehow like @else I guess: linked to the previous rule. But would only be valid immediately after a style rule.

I'm not sure I understand: statement one suggests the desire to nest two levels deep, but statement two say no extra levels of nesting...

@Myndex I think statement 2 refers to indentation levels

dbaron commented 1 year ago

I think the "post-nesting" examples (https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1277440033, https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1277440428) that use two levels of nesting are extremely confusing to read. Keeping nested selectors nested syntactically within the right pair of {} makes things much clearer, and we should continue to focus on finding an acceptable syntax that does that.

LeaVerou commented 1 year ago

I think the "post-nesting" examples (#7834 (comment), #7834 (comment)) that use two levels of nesting are extremely confusing to read. Keeping nested selectors nested syntactically within the right pair of {} makes things much clearer, and we should continue to focus on finding an acceptable syntax that does that.

Agreed. Can we just rule these out early (and those that introduce extra levels of nesting that have been ruled out in #4748) so we can keep the conversation more focused?

Loirooriol commented 1 year ago

I don't find François' proposal confusing, and I like that it clearly separates properties from nested rules, and the consistency with @else. Do you also find @else confusing? If not, what's the difference?

FremyCompany commented 1 year ago

I admit I also fail to see where the confusion stems from, but if a few people feel that way it might be good to try to understand the sentiment. Is that caused by the lack of whitespace? Usually, when I write CSS, I separate selectors by a while line, and I don't frequently make everything one-liner like in these examples.

To me, the mixing of declarations and selectors is significantly more confusing than using a different block. This is not clear to me at all at a glance, even with a single level of nesting; for instance:

a {
    color: blue;
    @nest;
    &:hover {
        color: red;
    }
}

(this remains true if we make the @nest optional, in my opinion, because it's not obvious at first sight that we have switched mode)

vrugtehagel commented 1 year ago

For what it's worth, you can essentially just take the "required @nest;" proposal, replace the @nest;s with } @nest {, and you've got François' proposal. If you don't like how the indentation goes back a level before nesting (which I would understand and agree with) the you can simply pretend like @nest; is a thing, but write it as } @nest {.

This (François' proposal):

a {
    color: blue;
}
@nest {
    &:hover {
        color: red;
    }
}

versus this (François' proposal, re-formatted):

a {
    color: blue;
    } @nest {
    &:hover {
        color: red;
    }
}

versus this (the @nest; proposal):

a {
    color: blue;
    @nest;
    &:hover {
        color: red;
    }
}

The benefit of } @nest { over @nest; is that there is no parser mode switch, and we can re-use the syntax construct we're using for @else.

To me, the mixing of declarations and selectors is significantly more confusing than using a different block.

I disagree with this, but this is just a personal preference; I believe it's quite easy for humans to tell declarations apart from selectors, and even if it wasn't, we use syntax highlighters to help us with this type of thing. The good thing about the } @nest { is that we can choose which we like more, and we can choose to dedent the @nest block to match the selector for the block it's nesting for, or we can choose to indent it and level the } @nest { with the declarations instead, like you would with @nest;.

Can we just rule these out early (and those that introduce extra levels of nesting that have been ruled out in https://github.com/w3c/csswg-drafts/issues/4748)

That issue did indeed rule out an extra level of nesting, but François' proposal doesn't do that at all. It introduces a separate block for the nested styles without introducing another nesting level. It's actually quite clever. And, let's try to give suggestions a fair chance - this is an open discussion and I think everyone's proposals deserve to be looked at and considered. If it does indeed fail to meet some criteria we've previously mentioned, we can rule them out. But this one, I don't think does that.

tabatkins commented 1 year ago

Every other context in CSS where something is nested in another thing, the rule is actually nested syntactically. Doing a sibling instead would be a huge departure in syntax, mental model, and CSSOM. Without a major advantage to this "sibling" approach (and major disadvantages for every other approach that actually nests), I'm happy to reject this approach out-of-hand.

Loirooriol commented 1 year ago

@tabatkins It's not a departure since we already have @else. I fail to understand how these arguments apply to "post" @nest but not to @else. I'm just puzzled because I don't see the problems, but it seems most people do, so I'm worried @else may need to change too?

bramus commented 1 year ago

@Loirooriol The difference though is that you wouldn’t nest an else within the if – it is a sibling to the if, so it makes sense to keep it like that (also see: any other programming language).

For nesting rules in rules, the logical thing to do, is to actually nest it syntactically. I mean, by making them siblings, CSS (as a language) would make a fool out of itself tbh – I can already see the 🤡-memes incoming.

And as Tab pointed out, we already use nesting a lot: nested at-rules, rules nested in at-rules (media queries, layers, scope, …), etc.

mirisuzanne commented 1 year ago

Yeah I find @FremyCompany's solution both odd for not being nested, and also somewhat elegant in reusing existing syntax. The departure is that @else is not related to one thing being inside the other, but is explicitly the opposite. Conceptually, selectors in @media are applied when 'inside' a matching media, and so on. Else appends a 'but otherwise' clause, which makes sense as conceptually un-nested. I go back and forth on it - don't hate it, but also likely wouldn't vote for it.

I've been thinking of this from a 'how would I teach this' perspective - and I think it would be easy to teach. But mostly I'm drawn to @LeaVerou's approach, which I would teach as: always start nested selectors with a symbol. The problem cases (elements with descendant combinator) aren't really an edge case, but they're generally straight-forward to fix using this rule. @scope and >> may also reduce the number of those cases.

(Even though - is technically a symbol, and part of declarations, it's not a symbol we allow at the start of selectors - so the rule holds for nesting.)

tabatkins commented 1 year ago

Yup, what Bramus said - if/else isn't expressing a parent/child relationship (it's alternatives at the same level), nor is it written in a nested fashion in virtually any programming language. (Certainly zero mainstream langs, but there are so many langs it's hard to say definitely nobody does this.) We very specifically rejected expressing @else with a nested grammar for that exact reason.

fantasai commented 1 year ago

I was confused when Lea described @FremyCompany's proposal, but seeing it, it kinda makes sense. Basically you have, potentially, two blocks associated with the selector: a block of declarations and a block of nested rules. That they're siblings isn't entirely unreasonable.

I still think @nest is noisy, and would prefer not having it. You could just drop @nest and have bare braces, though. Nesting is going to be so fundamental to CSS, it really needs to read cleanly.

article {
  color: gray;
} {
  h1 {
    color: black;
    font-weight: bold;
  } {
    span { color: pink }
    b { color: orange }
  }

  h2 {
    color: black;
  } {
    span { color: cyan }
    b { color: yellow }
    article & {
      font-size: 110%;
    }
  }
}
fantasai commented 1 year ago

@LeaVerou mentioned that @FremyCompany's proposal would mean the CSSOM and syntax would diverge, since the rules would be nested inside the style rule representation but be in a separate rule... but I think it makes sense to think of it not as a separate rule, but as giving a style rule an optional second block.

Conceptually, style rules would now have three parts:

Using bare braces or something like && makes this association between the selector and the second block clearer than using @nest; I think it's reasonably consistent with the desired CSSOM structure.

FremyCompany commented 1 year ago

Conceptually, style rules would now have three parts:

  • a selector
  • a declaration block
  • a style rule block (optional)

This is such a cleaner model, I love it :)

I am totally on-board with the idea of dropping @nest entirely, and just have the second block follow the first one.

fantasai commented 1 year ago

@FremyCompany It's a pretty clean mental model, but the downside for using bare braces is that the first declaration block is required, and having an empty pair of braces looks pretty awkward when you have nested rules but no declarations for the parent selector... Using something like && { rules } instead of { rules } solves that problem (you can prepend a selector with no ambiguity) but then people don't like excessive ascii...

LeaVerou commented 1 year ago

Using something like && { rules } instead of { rules } solves that problem (you can prepend a selector with no ambiguity) but then people don't like excessive ascii...

That would be extremely confusing for selector lists. E.g. here:

a, b && {
    .foo {
        /* ... */
    }
}

The "generated" selector is conceptually a .foo, b .foo (actually it is :is(a, b) .foo), but it looks like it would be a, b .foo.

LeaVerou commented 1 year ago

Folks, @fantasai and I worked on summarizing the current state of proposals (starting from @tabatkins' excellent summary here), you can find it here:

👉🏼 UPDATED SUMMARY TABLE OF SYNTAX PROPOSALS 👈🏼

We went with a MD file instead of an issue comment so that everyone can edit/send PRs.

In preparation for tomorrow's discussion, we think it would be useful to see where we stand in terms of consensus, so we added a table of participant preferences, and we encourage everyone to add their positions to the table as well (by edit or by PR)!

tabatkins commented 1 year ago

Apologies, but a non-nested nesting syntax is significantly a non-starter for me. It's a completely novel syntax form not attested by any other nested thing in CSS (native or preprocessor tools), and it comes with some significant learnability concerns (as commented above). As one of the spec editors, I don't find it an acceptable outcome.

FremyCompany commented 1 year ago

I am not happy with calling "non-nested" the nested-element selector in

element { property: value; } { 
    nested-element { ... }
}

It is nested in a pair of brace, just like it always has been in all proposals. It is just not the same pair of brace as the declarations. But, then again, CSS so far has always had blocks that contained only either declarations or rules.

I am not sure how "weird" this really is. This is exactly analogous to HTML.

<element property="value">
    <nested-element ... />
</element>

both the properties and the child elements are nested in element, but in two different context that are both associated to the same thing. When you don't need the nested elements, you can use <element /> which is what CSS has had so far.

I really don't see the weirdness in any of this, this is perfectly analogous with CSS.


PS: I am 100% fine with the group going with another solution than 4; honestly there are pros and cons to all. But I am not fine with having discussions where there are people "vetoing" options on the basis of personal preferences and justifying this using authority arguments like "as one of the spec editors". This is unprofessional. Please keep an open mind? 😕

tabatkins commented 1 year ago

It is nested in a pair of brace, just like it always has been in all proposals. It is just not the same pair of brace as the declarations.

Yes, that's the non-nested part I'm talking about. It's a sibling to the rule that it's trying to nest within.

PS: I am 100% fine with the group going with another solution than 4; honestly there are pros and cons to all. But I am not fine with having discussions where there are people stating that "because I am the editor, I don't want to hear about other people's opinion if they don't agree with me". This is unprofessional. Please keep an open mind? confused

This is not remotely what I said. Do not attribute offensive statements to me, particularly in a quoted fashion. Please rephrase or delete that comment.

FremyCompany commented 1 year ago

@tabatkins Sorry, you are totally right; I used quotes to express how I felt after reading your comment rather than your comment itself; this is not aceptable and I present my excuses for this.

romainmenke commented 1 year ago

Are the @nest blocks part of proposals 2 and 3?

I know enforcing verbose syntax isn't popular but some really like the low complexity of @nest (it just works).

.foo {
  color: green;

  @nest & anything {
    color: purple;
  }
}
mirisuzanne commented 1 year ago

Are the @nest blocks part of proposals 2 and 3?

I don't think they are at this point. I do think that could theoretically be added to 3 as an option (maybe also 2, but that's a bit weirder).

ydaniv commented 1 year ago

Conceptually, style rules would now have three parts:

a selector a declaration block a style rule block (optional)

I think a big pro with this approach proposed by @fantasai & @FremyCompany is that it minimizes the extra restrictions and further "parser no-go"s we'll have to impose on future syntax, and we know how much users hate that and find that confusing. I think, instead, it's rather another opportunity to maintain current syntax flexibility, with just a tiny compromise on nesting-syntax paradigm. Yes, it's a slight paradigm shift, sort of Functional vs. OO, and I think that is how we should try to approach it.

I also think that with a bit more of bikeshedding we can also overcome the .a, .b & {...} issue (I can already think of a few noisy ones 😋 )

tabatkins commented 1 year ago

Are the @nest blocks part of proposals 2 and 3?

Yes to the (2) proposals. (It's needed to trigger the switch when nothing else is around to do so.) No to the (3) proposal. (There's no switch at all, every rule is unambiguous on its own.)

romainmenke commented 1 year ago

Yes to the (2) proposals. (It's needed to trigger the switch when nothing else is around to do so.)

So both of these would valid in (2) : (question was specifically about at-rule blocks, not the block-less rule @nest;)

.foo {
  color: green;

  @nest & anything {
    color: purple;
  }
}

.foo {
  color: green;

  @nest;
  anything {
    color: purple;
  }
}

But @nest would not exist at all in (3)