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:

dbaron commented 1 year ago

So I'm generally happy with using @tabatkins proposal for token-based disambiguation (with a prefererence for some of the uses over others that I'll note in the summary table), but I'd like to suggest a small modification to it:

Basically, where the proposal currently says "anything except an ident", I'd like it instead to say "anything except an ident or a function". (It's possible that's what was intended in the first place.)

I think this is preferable because:

  1. Most authors of CSS probably don't understand the subtle tokenization distinction between ident and function, and I think we should avoid exposing it if we don't need to.
  2. It preserves some expansion space for properties in the future. For example, if we wanted a property syntax like min(width): 200px, we'd still be able to do that. (I don't think we actually want that... but we might want something syntactically like it.)
  3. I don't think adding functions to the "property" space hurts the disambiguation of selectors (although maybe I'm forgetting something). (Functional pseudo-classes begin with the : symbol and are not purely function tokens, just like :hover is not purely an ident.)
tabatkins commented 1 year ago

So both of these would valid in (2) :

Oh sorry, no, I was referring to just the no-op @nest;. @nest <selector> {...} is only part of (1).

tabatkins commented 1 year ago

Basically, where the proposal currently says "anything except an ident", I'd like it instead to say "anything except an ident or a function". (It's possible that's what was intended in the first place.)

Yes, I agree with your suggestion and reasoning. (I didn't actually intend it originally, but only because I didn't think about it at all.)

romainmenke commented 1 year ago

So both of these would valid in (2) :

Oh sorry, no, I was referring to just the no-op @nest;. @nest {...} is only part of (1).

I can follow that.

If @nest {} blocks were still part of (3) then code that was authored for (1) would still be valid and have the same meaning. But I think we can provide an automated one-time migration from proposal 1 to proposal 3. (This bit is not relevant to which proposal is better)

LeaVerou commented 1 year ago

I just realized a pretty significant disadvantage of the postfix proposals (4): all other nesting syntaxes allow for nesting in any CSSStyleDeclaration context, including inline styles, while this does not. I have now added this to the summary.

jonathantneal commented 1 year ago

I’m unsure whether to mark myself as preferring option 3.

From the description of option 3:

No parsing switch

I could support this, as I would prefer not to introduce new kinds of parsing states.

instead every nested rule has to be unambiguous on its own

I strongly support this, as it makes each rule feel more portable and self-sufficient.

by starting with anything but an ident.

I do not support this, as I could easily imagine future non-ident rules that should work differently. Ones that immediately come to mind are @if and @extend, and possibly some future version of @apply.

If there is a strong desire for minimal-but-strict parsing semantics, I would strongly recommend using only a very limited set of delimiters. I would propose supporting only one at first.

I would support limiting nesting to @-like rules. Using a bare @ has been mused, and it has received positive and negative signals.

.notification {
    color: blue;

    @ .context & {
        color: green;
    }
}

I think limiting nesting to an @ symbol would provide a strong and consistent signal to developers.

Here is another example applicable to this recent message:

.main-nav {
    display: flex;

    @ ul {
        list-style-type: none;

        @ li {
            margin: 0;
            padding: 0;
        }

        @ a {
            display: flex;
            padding: 0.5em 1em;
        }
    }

    @ &nav {
        display: block;
    }
}

Separately, additionally, and optionally, I think a bare @ at-rule could be a wonderful way to nest without implied descendant combinators, if it isn’t too confusing. Here is an example applicable to this recently popular CSS tweet:

.bubble {
    @:has(> &) {
        --triangle: down;
    }

    @::after {
        @container style(--triangle: down) {
            content: " injected";
            background: lime;
        }
    }
}

Here is one last contrived example where I will attempt to deliberately make it look bad, if it doesn’t already, and despite the fact that I recommend a bare @ symbol.

button {
    @:hover {
        color: black
    }
    @&:hover {
        color: black
    }
    @:is(:hover) {
        color: black
    }
    @&:is(:hover) {
        color: black
    }
    @&:is(&:hover) {
        color: black
    }
    @:focus, &:hover {
        color: black
    }
}

Note: In all of the provided examples where the bare @ is followed by a non-space selector, the bare @ is never being interpreted as the nesting selector (&). Rather, should we decide CSS nesting infers & at the start of each selector when none is present, and should we decide CSS nesting does not infer a descendant combinator, then we would just get this (helpful, I think) behavior for free.

LeaVerou commented 1 year ago

I do not support this, as I could easily imagine future non-ident rules that should work differently. Ones that immediately come to mind are @if and @extend, and possibly some future version of @apply.

My understanding (@tabatkins can confirm) is that these kinds of things would be fine, since the @-rule would be thrown away right now, as it's invalid, and since in proposal 3 these are not used as a parsing switch there are no side effects after the @-rule.

romainmenke commented 1 year ago

Having spend more time thinking about it I keep coming back to @nest blocks from proposal 1.

I like that proposal 3 introduces rules that make it possible to have relative selectors. I also like that those same rules make @nest blocks unneeded in all but one case (nested selector starts with a tag selector or an ident token).

But in a way @nest is similar to parentheses in calc(). It helps authors express what they mean without side effects and without requiring extra and very specific knowledge.

calc(10 + 3 * 5) is not ambiguous in any programming language because operator precedence is well defined. But not all authors find it easy to switch to this math context. It is ambiguous because we known about operator precedence and that left to right doesn't apply here, but we might not exactly remember how it all fit together.

Adding parentheses calc(10 + (3 * 5)) is more verbose, totally unneeded but it still helps a lot of authors to write and read this bit of code.

What I don't like about proposal 3 is that it forces authors to find their own way to write unambiguous selectors. There is no syntax provided by the language that is side-effect free.

It would be like having to write calc(10 + max(3 * 5)). You have to know that max() is fine with just one argument and that it doesn't have any other effects, but it is a very poor expression of wanting to disambiguate the calc expression. A future reader might attribute special significance to max which just doesn't exist in this context.


Invalid :

.some {
  /* any styles */

  dialog & {
    color: purple;
  }
}

Valid :

In these examples :modal is picked because it is a relatively new pseudo selector that is still invalid in a large portion of browser versions in use.

.some {
  /* any styles */

  :is(dialog) & {
    color: purple;
  }

  :is(dialog):modal & {
    color: purple;
  }

  /* this has a side-effect in theory, making copy-paste harder */
  :is(dialog:modal) & {
    color: purple;
  }

  :is(dialog):modal &, other & {
    color: purple;
  }

  /* this has an actual side-effect */
  :is(dialog:modal) &, other & {
    color: purple;
  }
}

Authors have to consider all the effects of :is() and all the effects of bits of their selector and exactly wrap the right part. But this information is then ambiguous to their future self and other authors. Did they intend exactly :is(dialog:modal) &, other & or did they just wrap the first compound selector because it looks logical.

I don't think :is() is suited for this purpose because it isn't side-effect free and it doesn't match what the author is trying to do (write a nested selector).

:where and :not is worse.

@nest blocks solved this elegantly. It provided a syntax for authors to express exactly what they intended without any side-effects.

Not saying that @nest has to be @nest exactly, but I think the problems it solved remain in part with proposal 3.

(this was also omitted from the twitter polls, authors weren't asked which syntax they prefer to resolve ambiguous cases)

devongovett commented 1 year ago

Is it out of the question to simply only allow (and require) & at the start of a nested selector? I feel the utility and readability of & in other positions isn't that great anyway. Maybe it's better not to nest these selectors but to write them out fully as separate non-nested rules. That would eliminate the need for @nest and simplify the syntax, while improving readability IMO.

sesse commented 1 year ago

all other nesting syntaxes allow for nesting in any CSSStyleDeclaration context, including inline styles, while this does not

Just to be clear, this is not intended to be in the current spec, right?

romainmenke commented 1 year ago

I feel the utility and readability of & in other positions isn't that great anyway.

I think a common use case of this is theming :

.foo {
  color: green;

  .some-class-for-blue-theme & { /* class likely added to `<body>` */
    color: blue;
  }
}

I don't think the direction should be to create an even more limited/strict syntax as authors clearly want a more flexible one.

In my opinion proposal 1 has the best tools to write correct selectors that match the authors intention. (this does not mean that they like writing them this way) But proposals 2 and 3 have relative selector syntax.

I think we can have both, not less than either.

ydaniv commented 1 year ago

I think a common use case of this is theming :

Then again, you could either have a stylesheet for that theme and have things nested there, or add that in a @layer (which doesn't exist in Sass), or even have the theme class in a separate rule and nest there

FremyCompany commented 1 year ago

I just realized a pretty significant disadvantage of the postfix proposals (4): all other nesting syntaxes allow for nesting in any CSSStyleDeclaration context, including inline styles, while this does not. I have now added this to the summary.

Isn't that an advantage instead? As far as I understand, doing this is not allowed in the current spec either. What would be the specificity of such a rule anyway?

sesse commented 1 year ago

What would be the specificity of such a rule anyway?

Even worse, what happens if you write <div style="@nest :is(&, .foo) { color: red; }">, would that affect an external element with class foo? This should be disallowed.

fd commented 1 year ago

As mentioned in #7796, there are some problems with the currently-proposed nesting syntax [...]

I believe the issues are not just syntactical in nature.


I've been following this thread for while now and I have some feelings... For argument's sake, I am going to assume that this proposal can actually be implemented correctly.

To summarize my understanding of the impact of the current proposal(s):

  1. No increased power in CSS's ability to style/layout pages. ie. plain CSS is equivalent to css-nesting and visa-versa.
  2. It has yet to be shown that the developer experience is dramatically better than that of plain CSS.
  3. More complexity in the CSS grammar.
  4. More API surface and complexity in the CSSOM.
  5. Tooling and IDEs will all need substantial changes to support nesting.
  6. This is a significant departure of the decades-old precedent that CSSRules are leafs in a stylesheet.

I feel there is insufficient justification for introducing all this complexity without first exploring the actual goals of this proposal. I believe that having clear answers to the following questions can improve the conversation around the nitty-gritty details of the proposal.

vrugtehagel commented 1 year ago

For 1, neither does e.g. @layer, or JavaScript's await keyword. Some feature exist just to improve developer experience and language ergonomics, and this is one of those features.

2 seems pretty clear to me - it has been shown time and time again that nesting is useful for maintainability of stylesheets. It is one of the main features of a CSS preprocessor. Nesting makes CSS easier to write, read, and understand. The benefit of introducing it to CSS is, of course, not needing preprocessors to do this work. An additional benefit is that stylesheets get smaller (because the selectors are shorter than if they were all written out). All together, there is no doubt about whether or not developers want nesting and what problems it would be solving.

3, 4 and 6 - yes, but nesting is a new construct and so this is unavoidable. The amount of complexity added is something we can try to minimize, but it's not something we can completely take away.

5 - yes, but this is irrelevant. IDEs and other tools adapt to the language, not the other way around.

To be clear, nesting solves the issue of all rules being syntactically identical. If I have 10 rules in a stylesheet with many more rules starting with .foo, then these rules are all scoped to be children of an element with class foo. This means something, but there's no syntactical distinction between these rules and the other rules. Nesting allows us to show, with syntax, that these rules belong together and form a meaningful group. Additionally, it removes the need to repeat ourselves; we no longer have to start each of these selectors with .foo, we can instead start them with just &. This makes the rules easier to find, read, and edit.

fd commented 1 year ago

@vrugtehagel thanks for your reply.


For 1, neither does e.g. @layer, or JavaScript's await keyword. Some feature exist just to improve developer experience and language ergonomics, and this is one of those features.

Agreed. I'm not arguing against quality-of-life features.

3, 4 and 6 - yes, but nesting is a new construct and so this is unavoidable. The amount of complexity added is something we can try to minimize, but it's not something we can completely take away.

Also agreed. Nonetheless this seems like a change in the spirit of CSS. Up to this point all CSS specs have treated CSSRules as leafs because they mark a switch in syntax/context. I think there is merit in keeping that property.

5 - yes, but this is irrelevant. IDEs and other tools adapt to the language, not the other way around.

I wouldn't call something that requires a community-wide effort to accomplish irrelevant. This will require a large amount of updating and reviewing of tooling to prevent a myriad of subtle bugs. And it will require educating the entire community on how to update their tooling so they don't have subtly broken setups.

2 seems pretty clear to me - it has been shown time and time again that nesting is useful for maintainability of stylesheets. It is one of the main features of a CSS preprocessor. Nesting makes CSS easier to write, read, and understand. The benefit of introducing it to CSS is, of course, not needing preprocessors to do this work. An additional benefit is that stylesheets get smaller (because the selectors are shorter than if they were all written out). All together, there is no doubt about whether or not developers want nesting and what problems it would be solving.

See below


To be clear, the way I understand your reply:

My main takeaways from this are:

  1. less repetition in selectors: This is largely addressed by :is and :where. @custom-selector or similar could also help with compound selectors. These solutions, i would argue, introduce way less complexity.
  2. grouping of code which share a selector: This is a nice but complicated thing to have. eg. not all rules fit neatly in one or another other group. So just as a code grouping tool nesting is not much better than using separate files.
  3. no preprocessors: I think it is safe to say that in the coming decade or more everyone will still be using preprocessors. Be it to minimize their code or polyfill a recent feature. Though a world without preprocessors would be nice, CSS is just not in a place where that is possible. I also believe it is fine for some QoL features to be preprocessor-only. This is why SCSS and variants are useful and popular.
  4. smaller size of stylesheets: The impact on the total amount of transferred bytes would be marginal. CSS selectors are highly compressible. Also remember nesting introduces many bytes of indentation, so I doubt that would even be true for uncompressed stylesheets.
devongovett commented 1 year ago

I think a common use case of this is theming

These days, theming is much better accomplished with variables (allows themes to be nested). But regardless, I find it easier to read if the nesting happens the other way (ie theme on the outside rather than nested within the component).

/* base styles */
.button {
  color: red;
}

/* theme styles */
.theme {
  & . button {
    color: pink;
  }
}
fantasai commented 1 year ago

@fd We've already decided to integrate nesting into CSS. This issue is just about improving the proposed syntax.

fd commented 1 year ago

@fantasai And yet I see much discussion on, and confusion around, the semantics of nesting. So I think it is fair to raise concerns about the proposals as they stand.

I'm not arguing that nesting should't exist. It just seems way to rough around the edges atm.

LeaVerou commented 1 year ago

Just to be clear, this is not intended to be in the current spec, right?

My understanding is that it is, and it's quite useful for things like:

<a style="color: blue; &:hover { color: red }">…</a>

What would be the specificity of such a rule anyway?

Even worse, what happens if you write <div style="@nest :is(&, .foo) { color: red; }">, would that affect an external element with class foo? This should be disallowed.

These are issues to be filed and resolved separately 😊

tabatkins commented 1 year ago

Nesting inside of style attrs is not yet in the current spec, but it is definitely intended as a future direction. The fact that there are additional issues to resolve is precisely why it's not in the spec quite yet. ^_^

(Being able to style :hover, ::before, etc from style will be super useful.)

fd commented 1 year ago

So just to be clear, a CSSStyleDeclaration has a childRules property with the nested rules?

LeaVerou commented 1 year ago

So just to be clear, a CSSStyleDeclaration has a childRules property with the nested rules?

I believe it would just be cssRules to match other CSSOM interfaces.

tabatkins commented 1 year ago

Yes, that's already in the spec.

fd commented 1 year ago

@tabatkins no it is not, you are referring to CSSStyleRule not CSSStyleDeclaration.

(also see what I mean by confusion?)

sesse commented 1 year ago

Nesting inside of style attrs is not yet in the current spec, but it is definitely intended as a future direction. The fact that there are additional issues to resolve is precisely why it's not in the spec quite yet. ^_^

(Being able to style :hover, ::before, etc from style will be super useful.)

We talked about it in the team earlier today; it seems rather unlikely that anyone in Blink would be interested in implementing nested rules on inline style. It would be a huge implementation headache that doesn't fit into anything with how we do inline style right now. But that is off-topic for this thread, probably.

css-meeting-bot commented 1 year ago

The CSS Working Group just discussed [css-nesting-1] Syntax Invites Errors.

The full IRC log of that discussion <fantasai> Topic: [css-nesting-1] Syntax Invites Errors
<fantasai> github: https://github.com/w3c/csswg-drafts/issues/7834
<emeyer> scribenick emeyer
<emeyer> astearns: fantasai has suggested we should focus on this call on whether we replace the text in the specification with option 3, and then have further discussion later on proposal 4.
<TabAtkins> +1
<lea> +1
<bramus> SGTM
<astearns> https://github.com/w3c/csswg-drafts/blob/main/css-nesting-1/proposals.md
<emeyer> astearns: We have four options with pros and cons, and some poll results
<emeyer> …Most positions are between options 3 and 1, with a smattering of 2 and 4.
<astearns> ack fantasai
<lea> I have added some vote counts under the table
<emeyer> fantasai: many considerations here. We have problems with the current syntax, which requires a lot of ampersands that are hard to remember where they’re needed
<emeyer> …we also want portability of the code structures is a concern with the current syntax
<emeyer> …there was another concern about confusion over spaces, but that seems minor
<emeyer> …Our options are: always prefix, nest into blocks, use a switch to change modes
<emeyer> …We explored many possible syntaxes. Lea?
<emeyer> lea: Option 3, you can nest without ampersands for most selectors
<TabAtkins> q+
<jensimmons> q+
<emeyer> …except the only descendants you need ampersands is element selectors
<argyle> q+
<emeyer> …The advantage is there’s no parser switch, and it’s also much less verbose than current syntax
<emeyer> …If you have a descendant selector that start with an element, you do need to ampersand that
<emeyer> …Compatibility with Sass is not a concern here, because it’s a one-time cost
<emeyer> …In general, the closer we can get to the Sass syntax, the better; developers like it
<emeyer> …People find the current syntax quite noisy
<emeyer> …We might also be able to relax syntax in the future, if we can figure out a way to do so
<emeyer> astearns: Ampersands not required, but allowed, yes?
<fremy> q+
<emeyer> (collective affirmation)
<emeyer> fantasai: Devs seem split 50/50 on use of ampersands or not
<emeyer> …As far as the proposal, the basic rule for nesting a selector is that you can’t start with a letter
<astearns> ack TabAtkins
<dbaron> (I think it's a letter or a dash?)
<lea> dbaron: yes
<emeyer> TabAtkins: While I initially supported option 1, having looked through the details, option 3 is very good
<emeyer> …It most of the time gives you the ability to write like in Sass, with one awkward exception that’s easy to tell apart
<bradk> Sorry to be so incredibly late
<emeyer> …Most of the time you can use an ampersand to nest and it works out fine
<emeyer> …The rule for whether you need it or not is very clear, and this does allow us to be close to Sass
<astearns> ack jensimmons
<emeyer> jensimmons: There are a lot of things about option 3 I really like, but the one thing I feel is a non-starter from my perspective is that not all selectors are treated the same
<emeyer> …Authors have to know that nesting is easy most of the time, except in this one case
<lea> Clarification: I didn't say element selectors were rare *at all*. Element selectors where the parent selector is inserted later (e.g. strong &) is rare.
<emeyer> …I understand the mitigations, the problem is teaching authors that this one selector type is weird, and the syntax isn’t consistent across all selector patterns
<astearns> ack argyle
<lea> q+
<emeyer> argyle: When you go to paste and forget the &, that’s a problem, but syntax highlighters are there to help authors
<emeyer> …Option 1 is the most portable because it’s unambiguous
<emeyer> …The community outside the WG has already passed the WG, which may be why a lot of people didn’t seem to case about the verbosity
<emeyer> …Option 1’s simplicity is appealing for tooling and to machines
<emeyer> …Teaching option 1 is easy, because it’s very consistent
<emeyer> …Teaching 3 or 4 gets muddy and complicated
<miriam> q+
<emeyer> …I did like the idea that most of the suggestions of 3 and 4 build on option 1, so I like the idea of going with option 1 and then in Level 2, we could use 3 and 4
<emeyer> …This would let us unblock engines that already do option 1 now
<fantasai> 4 doesn't build on 3 or 1 or anything
<TabAtkins> 3 has exactly one exception, same as 1 (and imo of the same complexity). 4 has no exceptions. It was the 2 variants that had slightly more complex requirements.
<fantasai> and also doesn't have any exceptions
<astearns> ack fremy
<argyle> correct, 4 doesnt build on 1, my bad
<emeyer> fremy: I’m glad that we aren’t considering option 2. I’m mixed between 1 and 3, and agree with the points mentioned before
<emeyer> …I’m kind of okay with 1, would be fine with 3, the consistency of 1 feel better
<emeyer> s/feel/feels/
<emeyer> If I had to choose, I’d probably say 1, but wouldn’t be mad if we choose 3
<emeyer> …I see myself converting nested rules into scoped rules, which we aren’t really addressing
<argyle> memorizing idents shouldnt be a requirement to nest effectively..
<emeyer> …Having a syntax incompatible with :scope is annoying and a downside
<astearns> zakim, close queue
<Zakim> ok, astearns, the speaker queue is closed
<emeyer> …You shoudl be able to copy and paste anything and not have to rewrite anything
<astearns> ack lea
<emeyer> lea: Replying to jensimmons, I didn’t say element selectors are rare, but that element selectors where the ampersand comes later are rare
<astearns> ack miriam
<emeyer> miriam: Both proposals have weird edge cases
<fantasai> lea mentioned that `strong &` is rare, but `& strong` is fairly common and can be done with & as today
<emeyer> …in option 1, you have to learn when to use or not use @nest
<emeyer> …in option 3, you have to learn when to use ampersands with a descendant
<emeyer> …In my mind it’s a little easier to teach, because I can teach that you always have to start with a symbol
<fremy> miriam: we could just replace `@nest` with `&` and say that if there are one at the start and one later one, the one later one wins
<emeyer> …I do see Adam’s point that 1 and 3 are interoperable if we want them to be
<emeyer> …I don’t know if that’s an interesting approach to take, but it is possible
<jensimmons> I wish we had time to talk about option 4. If we ended up like 4 best, there's no need to debate 1 vs 3, etc.
<emeyer> astearns: I think it’s intriguing to do 1 and then allow 3 in the future
<bradk> I agree with the idea that #3 still allows you to do #1
<emeyer> …We need to take this back to the issue and come back to this next week with another scoped-time discussion
<emeyer> …Please do discuss further in the issue.
romainmenke commented 1 year ago

…in option 1, you have to learn when to use or not use @nest …in option 3, you have to learn when to use ampersands with a descendant

In option 3 you also have to learn when and how to fix .foo { bar & {} } as I stated above : https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1283489362

Doing option 3 as an evolution of option 1 doesn't have this issue and still gives us relative selector syntax and the copy-paste benefits that come with that.

Also see : https://github.com/w3c/csswg-drafts/issues/7854

FremyCompany commented 1 year ago

Just because this will be burried in the call otherwise, I also came up with another variation of 1 that might solve its weirdest syntactic edge-case is: getting rid of @nest and allow & to appear in the selector twice (one at the start, and optionally one inside which is the actual place the replacement occurs in the rare cases where you need that). Every selector always start with & that way, this is consistent and easy to teach. I guess this would cont as 1b in the proposal nomenclature.

I would be fine with that as well, especially if we combine this with a compatibility change to @scope to allow & as a :scope shortcut. That way, we have a consistent syntax, and solves the nested>@scope upgrade path. This is #7854 which @romainmenke mentioned just before.

I would still rather we had 4 which requires no parser update and allows for bi-direction nested-to-@scope compatibility (in fact, you can just take any part of css and nest it, no change needed), but there are other trade-offs of course, which I care less about but might be of interest to others I reckon.

Loirooriol commented 1 year ago

I don't like & having different meanings in different parts of the selector, very confusing.

romainmenke commented 1 year ago

Just because this will be burried in the call otherwise, I also came up with another variation of 1 that might solve its weirdest syntactic edge-case is: getting rid of @nest and allow & to appear in the selector twice (one at the start, and optionally one inside which is the actual place the replacement occurs in the rare cases where you need that). Every selector always start with & that way, this is consistent and easy to teach. I guess this would cont as 1b in the proposal nomenclature.

Using & multiple times is valid :

.foo {
  &:has(> &) {}
  &:is(& .other) {}
  & + & {}
}
flackr commented 1 year ago

Option 4 makes nesting within nesting harder to read IMO:

E.g.

.a {
  .b {
    .c {

    }
  }
}

If you were to try to write this with the postfix option 4 I think it is this:

.a {

} & {
  .b {

  } & {
    .c {

    }
  }
}

Edit: Updated to match proposal syntax - it is possible.

FremyCompany commented 1 year ago

@flackr No. This was discussed before, you don't actually have that issue.

.a { property: value; } {
    .b { property: value; } {
        .c { property: value; }
    }
}

or (more classically)

.a {
    property: value;
    property: value;
} {
    .b {
        property: value;
        property: value;
    } {
        .c { property: value; }
    }
}
LeaVerou commented 1 year ago

If the goal is to do option 3 as an evolution of the current syntax, I think we should drop @nest entirely and just say "you can't do that in level 1, nested selectors just have to start with an ampersand". Otherwise we are stuck with @nest forever.

In option 3 you also have to learn when and how to fix .foo { bar & {} } as I stated above : #7834 (comment)

This is extremely rare however. I think it's fine to have weirdness in rare edge cases if it allows the common cases to have better DX.

romainmenke commented 1 year ago

That sounds like making the simple cases easier and the hard cases much harder. I'd rather have a syntax that works well without such surprises.

I seem to have totally missed what is wrong with @nest aside from it not being popular to type. Proposal 3 is awesome because it allows authors to avoid it most of the time. But I fail to see why it must not exist at all.

Something that is rare, multiplied by all the CSS authors over the entire future of CSS is not very rare at all.

FremyCompany commented 1 year ago

I don't like & having different meanings in different parts of the selector, very confusing.

Yeah, I am not a huge fan myself to be fair, but this has the advantage of removing @nest which is weird to teach in 1 and 3. But it's not impossible to teach either, I'm not saying we have to do this, just that I thought of that option as a way to alleviate some concerns I have heard others mention.

Again, I think we are facing trade-offs here. I am just trying to propose new ideas to see if something else emerges that everyone can get behind. (I personally haven't changed opinion that 4 gives us the most teachable and interoperable syntax.)

fd commented 1 year ago

How would prop 3 handle the following case? Obviously this example is incorrect CSS. But how should an implementation treat it? Specifically, what is #bad supposed to mean? Is it a <hex-color> or an id-selector? How can this be made as backwards-compatible as possible?

.foo {
  background-color: #bad 
  .bar { 
    outline: red solid 1px;
  }
}
tabatkins commented 1 year ago

That is a single declaration for background-color with the value #bad .bar {...}, finally ended by the close brace of its containing rule. Declarations don't end until they see a semicolon or their parent rule's closing brace. Leaving off your ending semicolon has always resulted in an invalid declaration unless it happened to be at the end of a rule.

(There's nothing prop-3-specific about this, this is just the existing behavior of how declarations are parsed.)

fd commented 1 year ago

Declarations don't end until they see a semicolon or their parent rule's closing brace.

Yes exactly, except the current value parsers/tokenizers can, and often do, assume that any closing brace belongs to the parent rule. Nesting changes this in a subtle way. So value parsers/tokenizers now need to track the parity of braces.

I think these scenarios needs some text, just to make sure we have rules to work with.

tabatkins commented 1 year ago

No current browser parser/tokenizers naively scan for the next }. If existing non-browser tools do, they're violating the Syntax spec. Correctly handling nesting of (), [], and {} has been part of CSS since at least the CSS2 grammar, and it's extremely explicit in the Syntax spec.

So this is all completely well-defined in CSS. The only parsing detail relevant here is that nested rules will use my described "consume an ambiguous rule" algorithm, which will end them immediately (and throw the result away as invalid) if they see a (top-level) semicolon in the selector part.

BenjaminAster commented 1 year ago

I might be a bit late to this discussion, but I just had yet another (radical) idea that completely changes the syntax of nested rules. Note that this may or may not be a good idea.

My syntax looks like this:

.container {
    margin: 1rem;
    border: 1px solid red;

    (.child:
        color: black;
        padding: .5em;

        (&:nth-child(even):
            text-decoration: underline;
        )
    )

    (> .direct-child:
        font-style: italic;
    )

    @media (width > 500px) {
        background-color: darkGreen;
    }

    (&:hover:
        opacity: .8;
    )

    (:root[data-theme="blue"] &:
        color: blue;
    )
}

To make the syntax of top-level rules more consistent with nested ones, one could perhaps also change top-level rules to this

(selector:
    property: value;
)

-syntax.

fantasai commented 1 year ago

Fwiw, if we go with option 4 and want to use something other than bare parentheses, I would not want to us & since it already has a meaning as a selector. We could use && or something else, but not & by itself. So, to use @flackr's example, it would look like:

.a {
  property: value;
  property: value;
} && {
  .b {
    property: value
    property: value;
  } && {
    .c1 {
      property: value;
      property: value;
    }
    .c2 {
      property: value;
    }
  }
}

Bare parens are cleaner, but one advantage of using an indicator rather than bare parens is being able to drop empty declaration blocks, so instead of

.a {} {
  .b { ... }
}
/* or */
.a {
} {
  .b { ... }
}

you can write

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

which maybe looks less weird, idk. (Nested rules without declaration blocks on the parent selector are reasonably common.)

Loirooriol commented 1 year ago

I think && looks very weird and I don't see the advantage, if you want to omit the declaration block then writing && doesn't seem much simpler than {}. So I prefer bare parens.

FremyCompany commented 1 year ago

Here are the slides from the presentation I gave today CSS NESTING PROPOSAL 4.pptx

Ah, and we forgot the github link, so the minutes didn't get published here, here are there:

The CSSWG discussed this issue during a call
``` [16:19] fremy: This proposal says there can be a group of rules, no need to change parsers and it’s easy for developer tools [16:19] …You can have a selector, a declaration, then a group of rules [16:20] …Why would we do this? Three main advantages: [16:20] …1. You can copy and paste without making changes [16:20] …2. This is parsing as it’s done today [16:20] …3. This is exactly what CSSOM will do [16:21] …Because there’s nothing special, if you’re writing normal CSS, this is as easy as writing your first rule, and just have to tab things over [16:21] …This is exactly the same thing you do for @scope [16:21] …WIth this proposal, because everything uses the same syntax, you can reuse your CSS [16:22] …It’s CSS as you’ve always written it [16:22] present+ [16:22] …You don’t have to change the style declaration type [16:23] …Here, the declaration and rules are exactly the same, no need to change [16:23] …If you’re a tool developer, this is much easier [16:23] …You don’t have to switch between parsing modes based on changes of syntax [16:24] …This also avoids CSSOM incoherence [16:25] (scribe loses the plot in the weeds) [16:25] fremy: This is what the Blink people want to implement [16:27] Rossen: we won’t take a decision just yet, but want to get through the queue [16:27] lea: Inline styles are in scope for nesting for the future, so it’s a disadvantage of this proposal that nesting doesn’t support inline styles [16:28] …I don’t think it’s really that much easier for tool developers [16:28] …If this fits with CSSOM, the CSSOM design may not be ideal [16:28] fremy: For inline styles, you can easily add a new "cssrules" attribute, which maps to the second group. [16:28] ...In my opinion this is a better design than mixing. [16:29] Rossen: thanks to fremy for the introduction ```
FremyCompany commented 1 year ago

Something that still bugs me about the proposals 1 and 3 is the inconsistency.

element { ...; .class { ... } }
element { ...; & element { ... } }
element { ...; @nest element & { ... } } /* or element { ...; } { :is(element) & { ... } } */

three different syntaxes for three reasonable use cases, versus

element { ...; } { .class { ... } }
element { ...; } { element { ... } }
element { ...; } { element & { ... } }

one syntax for all cases

LeaVerou commented 1 year ago

@FremyCompany Option 3 doesn't have @nest. The third one would be :is(element) & in Option 3, though these kinds of selectors (where the ampersand comes after an element selector is fairly rare).

css-meeting-bot commented 1 year ago

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

The full IRC log of that discussion <astearns> topic: nesting syntax
<astearns> github: https://github.com/w3c/csswg-drafts/issues/7834
<ydaniv> astearns: who wants to present summary
<ydaniv> TabAtkins: we've got 4 options covering all the posibilites
<fantasai> Summary of Options -> https://github.com/w3c/csswg-drafts/blob/main/css-nesting-1/proposals.md
<ydaniv> ... option 1: either start with @nest or &
<ydaniv> ... option 2: you have a parser switch and afterwards only declaration switch
<fantasai> s/only declaration switch/only style rules, so they're unambiguous and don't need special prefixing/
<ydaniv> ... option 3: your selector has to start with anything but an ident
<ydaniv> ... in a rare case where you do need to start with ident you can either use an & or wrap it up with :is() or :where()
<ydaniv> ... option 4: what FRemy presented either as separate block or separate block following an &
<ydaniv> astearns: we've had a poll. I wonder whether there's another combination of starting with 1 and relaxing later to 3.
<ydaniv> TabAtkins: not sure it's a good idea to change syntax
<astearns> ack fantasai
<Zakim> fantasai, you wanted to respond to astearns
<fremy> q?
<fantasai> -> https://github.com/w3c/csswg-drafts/blob/main/css-nesting-1/proposals.md#twitter-polls
<ydaniv> fantasai: lea did a couple of twitter polls which are also helpful
<ydaniv> ... we have a significant majority that ppl perfer & at all time
<ydaniv> ... we have a large number of authors who are not going to be satisfied with option 1
<argyle> q+
<fantasai> s/perfer & at all time/prefer not requiring & at all times/
<jensimmons> q+
<astearns> ack argyle
<ydaniv> adam: there are many other projects with different syntax who are using different syntax and are not considered in the polls
<TabAtkins> (Tho note there are even larger ecosystems that are using nesting syntaxes without a required & prefix.)
<lea> q+ to say "authors are not complaining" is not equivalent to "authors are satisfied"
<astearns> ack jensimmons
<ydaniv> ... just wanna make sure they're included in this conversation
<fremy> q+
<jensimmons> Meet is broken
<astearns> ack lea
<Zakim> lea, you wanted to say "authors are not complaining" is not equivalent to "authors are satisfied"
<astearns> q+ jensimmons
<ydaniv> lea: authors are not complaining, they just accept it is what it is. But I have seen authors complaining that current syntax is too noisy
<argyle> var(--foo) got the same response
<ydaniv> ... this is why we brought this up
<ydaniv> ... they really need nesting, but that it could be better
<emeyer> q+
<astearns> ack jensimmons
<fantasai> s/that it could be better/but they will use what they have, even if it could be better/
<ydaniv> jensimmons: I really like option 4 the best. It's really simple. Other options are really complicated. I feel like CSS should be fun and simpe
<florian> +1 to jensimmons
<astearns> ack fremy
<ydaniv> ... option 4 captures that
<ydaniv> fremy: I agree with what lea said. At the end of the day you have to ship something.
<argyle> q+
<ydaniv> ... ppl use what they have
<ydaniv> ... I would not object to option 3. It's not the best I think. I like 4 better.
<fremy> https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1292196313
<ydaniv> ... one point I want to highlight, option 4 is the least complexity
<ydaniv> ... we define 3 different syntaxes for nesting. It may change...
<jensimmons> q+
<TabAtkins> q+
<ydaniv> ... You have to consistently switch between syntaxes. I think this is a big weakness
<flackr> q+
<astearns> ack emeyer
<ydaniv> ... why add ambiguity when we can keep it simple. But if other ppl prefer other options and are comfortable with tradeoffs then we might as well do it
<ydaniv> ericmeyer: I like option 4. Makes the most sense as an author.
<astearns> ack argyle
<lea> q?
<ydaniv> argyle: may pick whatever syntax via any tool they choose.
<miriam> q+
<ydaniv> ... when you reason option 1 has the least things to remember. The least ambiguous
<ydaniv> ... feels like a win win win across the board
<fantasai> Option 4 is the most unambiguous, and quite likely the easiest to implement.
<fantasai> Whether it's the best, idk, but it's the most unambiguous
<lea> Not sure where it's founded that other options would take longer to implement. We only have one data point here.
<ydaniv> ... you don't have to remember what you can/can't use. Not need to teach what's an ident
<ydaniv> ... all options eventually rely on &
<ydaniv> fremy: not true, option 4 does not
<astearns> ack jensimmons
<ydaniv> jensimmons: I don't really like the idea of us coming up with syntax now and changing later.
<TabAtkins> Big +1 to jen's point - adding new *functionality* later is fine, adding new syntaxes for no new functionality is bad.
<fantasai> +1
<ydaniv> ... as someone who's taught authors, I don't think it'll go well if we change syntax 3 yrs down the road
<ydaniv> ... our best shot is now
<lea> +1
<ydaniv> ... we should plan on our very best solution and be done with it
<ydaniv> ... I also believe strongly that nesting is important for ppl to code better and easier
<ydaniv> ... if they have to remember many rule with a lot of mental complexity it's an overhead
<ydaniv> ... not assume that using other tools is something everyone will be okay with
<fantasai> jensimmons: I want nesting to be easy and fast to use, I want it to be elegant, and if there's a bug it's a typo and not because you're using the wrong variant of syntax
<bramus> q+
<ydaniv> ... option 4 we haven't talked much about before. I haven't heard others talking about it much
<astearns> ack TabAtkins
<ydaniv> TabAtkins: I'd like to say what I don't like about option 4
<ydaniv> .... I'm very strongly opposed to 4. 1. it is not nested, feels unnatural
<bramus> +1
<JakeA> +1 to the it's-not-actually-nested issue
<ydaniv> ... the idea that it's chained after and not inside the block
<bramus> authors already know how to nest, from at-rules, markup, etc.
<ydaniv> ... option 4 is the only that's not compatibe with current state. It doesn't mix with the rest.
<florian> q?
<florian> q+ to respond to tab's argument 1 and 2
<fantasai> wrt style attributes, we could just allow the rule block to be placed inside the style attr, if we don't want to add a new attribute. This has the same parsing properties.
<ydaniv> ... if you want nest existing rules into other rules you have to go to bottom and add there
<lea> +1 to everything TabAtkins is saying
<ydaniv> ... in general having things in a sibling-wise way is not a greay idea
<florian> q-
<ydaniv> ... in parent-child relationship things are always nested. other options do that, 4 does not
<ydaniv> ... I also want to stress that this is harder to parse
<fantasai> s/is harder/is not harder/
<ydaniv> ... 1 & 3 are identical. 2 is a bit harder
<ydaniv> ... 4 isn't difficult in any way, simply new
<fremy> q!
<astearns> ack florian
<astearns> ack fla
<ydaniv> flackr: we mentioned in option 3 that it's possible to relax further.
<astearns> q+
<ydaniv> ... it may be, but not a good idea
<ydaniv> TabAtkins: not a good idea ^_^
<astearns> ack miriam
<fantasai> s/good idea/possible idea/
<fantasai> TabAtkins: Selectors just have fundamentally infinite overlap with property:value delcarations
<ydaniv> miriam: I agree generally with what Tab said. option 4 is weird. It's all trade offs. 3 is the one I voted for.
<ydaniv> ... there's one rule to learn, always start with a symbol
<ydaniv> ... I like the idea that both allow for forgiving parsing
<astearns> ack bramus
<astearns> ack dbaron
<ydaniv> bramus: I think nesting without nesting is also inventing wierd things
<ydaniv> dbaron: I think that 4.iii variant is somewhat different than others. Having look at it more, looks like you have a selector and bunch of things after it
<ydaniv> ... after looking it while doesn't feel like a nested rule
<ydaniv> astearns: I was on the queue to say something similar
<dbaron> s/a nested/an un-nested/
<fantasai> -> https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1282700284
<fantasai> Conceptually, style rules would now have three parts:
<fantasai> a selector
<fantasai> a declaration block
<fantasai> a style rule block (optional)
<TabAtkins> that extra indent is very significant!
<ydaniv> ... we also had another similar proposal of nesting a new block inside
<astearns> q?
<astearns> ack astearns
<ydaniv> ... which gets rid of the weirdness
<astearns> ack fantasai
<ydaniv> fantasai: my suggestion we have a lot of options
<ydaniv> ... pick between 1 or 3 and then continue
<ydaniv> astearns: we're not picking a winner, we're choosing which avenue to take
<ydaniv> TabAtkins: I suggest we take binding votes
<bramus> +1 on binding vote
<ydaniv> fantasai: I think the problem is that ppl are trying to implement and we should be trying to resolve today
<ydaniv> ... suggest polling between 1 and 3
<fantasai> POLL: Option 1 vs Option 3
<TabAtkins> 3
<lea> 3
<fantasai> 3
<argyle> 1
<oriol> 1
<miriam> 3
<JakeA> 1
<flackr> 3
<dbaron> 3
<patrickangle_> 3
<futhark> 1
<bramus> 3
<jensimmons> 3
<ydaniv> astearns: taking 2 votes: whether to continue with 1 or change to 3 please put
<astearns> 3
<fremy> 3
<Sebastian_Zartner> 1
<emeyer> 3
<ydaniv> 3
<florian> 3
<ydaniv> astearns: looks very much in favor of 3
<ydaniv> fantasai: we're taking option 3 over 1
<ydaniv> RESOLVED: We're taking option 3 over option 1
<ydaniv> fantasai: anyone want's to add anything to this discussion?
<fantasai> s/this/3 vs 4/
<dbaron> s/this discussion/the discussion of option 3 versus option 4/
<ydaniv> astearns: I think that option 4 may get us to a better place
<ydaniv> fantasai: option 4 has to possible ways...
<astearns> the added punctuation could be '{}' :)
<ydaniv> s/to/two/
<fremy> q+
<jensimmons> q+
<dbaron> fantasai: ... two adjacent blocks, or some punctuation between them (which would allow omitting the first block)
<fantasai> s/to possible ways/two viable options/
<ydaniv> fremy: there's one thing I dislike, the idea that this is usability against "I don't like it"
<ydaniv> TabAtkins: I think this is a bad characterisation
<TabAtkins> q+
<astearns> ack jensimmons
<astearns> ack fremy
<ydaniv> jensimmons: I heard ppl say that option 4 doesn't feel like nesting because they're not nested inside. not like SASS.
<ydaniv> ... but I feel like I get that, I hated it, but then thought what it opens up, it became my first choice
<ydaniv> ... it lets me write more simply without repeating
<TabAtkins> Big objection against 4 is the inconsistency with other syntaxes or planned future syntaxes. At-rules - do they have to go in the property or the rule block? Nested @media - how do we do properties directly inside of it with option 4 syntax? This is common and popular in Sass, for example.
<ydaniv> ... feels like we're trying to solve reality where instead we could choose a different way to accomplish more
<astearns> ack TabAtkins
<ydaniv> TabAtkins: I said before that option 4 is incossistent with current syntax and others
<lea> Oh great point, +1 TabAtkins
<futhark> present-
<ydaniv> ... introducing a new postfix syntax will cause us problems in may ways
<astearns> ack fantasai
<Zakim> fantasai, you wanted to respond to Tab
<ydaniv> ... instead of using current syntax which works nicely
<ydaniv> fantasai: I agree with Jen about portability between contexts
<ydaniv> ... I have reservations against, options 3 has more rules to learn but portability troubles me
<ydaniv> ... we can make changes to syntax
<lea> q?
<ydaniv> ... there are ways around nesting media rules
<TabAtkins> 3
<JakeA> 3
<bramus> 3
<lea> 3
<argyle> 3
<fremy> 4
<astearns> 4
<ydaniv> astearns: shall we take 3 vs. 4 poll? please vote
<oriol> 4
<jensimmons> 4
<flackr> 3
<florian> 4
<dbaron> 4
<andreubotella> 3
<patrickangle_> 4
<Sebastian_Zartner> 3
<ydaniv> 4
<fantasai> probably 4
<jensimmons> ericmeyer isn't here, but he said 4 before
<dholbert> 4
<ydaniv> TabAtkins: editors choice
<ydaniv> lea: which editor?
<ydaniv> astearns: slightly in favor of 4
<ydaniv> ... if we want to take a binding resolution 4 wins
<ydaniv> TabAtkins: I would object, it's very annoying.
<ydaniv> argyle: I agree
<JakeA> Poll developers for 3 vs 4?
<ydaniv> astearns: we could change the draft to 3, and leave this question open
<ydaniv> florian: it's effectively making a decission
<argyle> its just a prototype, no intent to ship
<ydaniv> astearns: if we're conflicted then we should not ship
<ydaniv> lea: it's already implemented behind a flag
<ydaniv> astearns: then they're shippind shouldn't affect our decission
<ydaniv> jensimmons: I would be disappointed if google ships and this affects our process
<ydaniv> argyle: Google never intended to ship
<ydaniv> ... no one was intending to ship
<ydaniv> jensimmons: pressure is good
<astearns> ack fantasai
<ydaniv> fantasai: I think we should take suggestions to put options infront of developers and get more feedback
<ydaniv> ... we could get more colab
<ydaniv> ... would be reasonable to update draft to 3 think we like it better
<ydaniv> ... get more feedback about 4 and come back when we're more confident
<JakeA> The future is longer than the past :D
<ydaniv> ... we have to get it right, otherwise...
<ydaniv> jensimmons: what Tab said about syntax sounded intesresting would like to see example
<fantasai> s/otherwise/this is going to fundamentally change how we write CSS for ~80% of style rules for the rest of the life of CSS/
<fantasai> s/think we/since we/
<fantasai> s/like it better/like it better than option 1/
<ydaniv> argyle: I sugges writing many examples and see how it comes up
<fantasai> +!
<fantasai> +1
<ydaniv> ... with option 4 & 3
<ydaniv> astearns: proposed resolution to change draft to 3 and leave 4 to another talk
<ydaniv> ... I don't think it's useful anymore
<ydaniv> RESOLVED: change the spec to reflect option 3
<ydaniv> astearns: who will take this to developers?
<ydaniv> lea: together with someone else
<ydaniv> fantasai: I can help
<ydaniv> miriam: me too
<ydaniv> fantasai: we should also mark it up in the draft for ppl to see
<ydaniv> astearns: done with this issue
<ydaniv> ... 10 mins break
<fantasai> s/it up in/issue into/
sesse commented 1 year ago

I'm trying to get a handle on proposal 3, more specifically this idea of implicit & insertion (which I really think is a bad idea, but people seem to feel very strongly about it). Could we formulate clearly when & is inserted and when it's not, in a way that doesn't require lookahead to specify what is a valid rule? For instance:

And finally:

sesse commented 1 year ago

Even more: