Closed fantasai closed 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:
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.):
symbol and are not purely function tokens, just like :hover
is not purely an ident.)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).
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.)
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)
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.
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.
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.
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)
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.
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?
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.
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
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?
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.
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):
CSSRule
s 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.
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.
@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 CSSRule
s 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:
:is
and :where
. @custom-selector
or similar could also help with compound selectors. These solutions, i would argue, introduce way less complexity.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;
}
}
@fd We've already decided to integrate nesting into CSS. This issue is just about improving the proposed syntax.
@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.
&
required vs &
optional (different semantics)@nest
a verb or is it @nest
-ed? (CSS doesn't have verbs)I'm not arguing that nesting should't exist. It just seems way to rough around the edges atm.
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 😊
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.)
So just to be clear, a CSSStyleDeclaration
has a childRules
property with the nested rules?
So just to be clear, a
CSSStyleDeclaration
has achildRules
property with the nested rules?
I believe it would just be cssRules
to match other CSSOM interfaces.
Yes, that's already in the spec.
@tabatkins no it is not, you are referring to CSSStyleRule
not CSSStyleDeclaration
.
(also see what I mean by confusion?)
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 fromstyle
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.
The CSS Working Group just discussed [css-nesting-1] Syntax Invites Errors
.
…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.
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.
I don't like &
having different meanings in different parts of the selector, very confusing.
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) {}
& + & {}
}
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.
@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; }
}
}
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.
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.
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.)
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;
}
}
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.)
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.
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.
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.
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.)
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.
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:
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
@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).
The CSS Working Group just discussed nesting syntax
, and agreed to the following:
RESOLVED: We're taking option 3 over option 1
RESOLVED: change the spec to reflect option 3
RESOLVED: open new issue for 3 vs 4
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:
.foo { .bar { … }}
— seemingly this should be interpreted as & .bar
(again, I think we should not do this; the logical thing is to interpret it as &.bar
, but I'll leave it for now)..foo { .bar & { … }}
— this should not be interpreted as & .bar &
, because there's already a & in there (it's nest-containing)?.foo { > .bar & { … }}
— however, this should be interpreted as & > .bar &
, because it starts with a combinator? Or should this be a parse error? (Why?).foo { :hover { … }}
— This should be & :hover
and not &:hover
? But I've seen examples where people seem to assume it's the latter..foo { :is(&) { … }}
— Should this be interpreted as & :is(&)
or just :is(&)
? I would assume the latter, since it's nest-containing, but you may have to dig pretty deep to find the &..foo { :is(.bar, !#&/) { … }}
— Is this nest-containing (which then decides whether there should be a &
in front? There's an & in there, but it's dropped in forgiving selector parsing.And finally:
.foo { .bar { … }}
— how do we serialize this?.foo { & .bar { … }}
— how do we serialize this?.foo { > .bar { … }}
— how do we serialize this?.foo { & > .bar { … }}
— how do we serialize this?Even more:
.foo { .bar &, .baz { … } }
— I assume this should be interpreted as .bar &, & .baz
?.foo { :is(.bar &, .baz) { … } }
— but this should be interpreted as :is(.bar &, .baz)
, so not consistent with the previous one?.foo { > .bar, .baz { … } }
— and this should be & > .bar, & .baz
?.foo { > .bar, + .baz { … } }
— and this should be & > .bar, & + .baz
?
Edit: 👉🏼 👈🏼 (from https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1282776786 )
As mentioned in #7796, there are some problems with the currently-proposed nesting syntax:
@scope
, which invites a lot of copy-paste errors when transferring code among these contexts&
within all the selectors in a list even beyond syntactic disambiguation (instead of allowing relative selectors) is annoying to type, easy to forget, and makes automated (and manual) conversion of existing code much more difficult (requires selector parsing rather than regex).&.foo
and& .foo
are easily confusable (both for reading and for typing), and so long as the latter is required for nested relative selectors this will be a commonly encountered problem