csswg-drafts icon indicating copy to clipboard operation
csswg-drafts copied to clipboard

[css-cascade] [css-nesting] Figure out whether we're fine with "shifting up" bare declarations after rules

Open emilio opened this issue 1 year ago • 61 comments

If you do:

div {
  color: green;
  @media (width > 0) {
    color: red;
    background: red;
  }
  background: green;
}

My understanding is that per spec the div color and background would be red.

That seems rather confusing. There are various alternatives here:

  • We're ok with this.
  • We forbid declarations after nested rules.
  • We deal with bare declarations by somehow sorting them together, so that would be effectively something like:
div {
  color: green;
  @media (width > 0) {
    color: red;
    background: red;
  }
  & {
    background: green
  }
}

Maybe something else?

cc @fantasai

emilio avatar Apr 19 '23 15:04 emilio

We forbid declarations after nested rules

Seems potentially problematic that some people may have some garbage followed by lots of declarations, then at some point we add some feature that parses the garbage as a nested rule, and then all the declarations stop applying. Spooky action at distance.

One of the main characteristics of option 3 (later modified with lookahead) is that it allows freely mixing declarations and nested rules. So at this point I don't think it makes sense to restrict that. The proper way to have such restriction would have been choosing option 4 or similar (which BTW seemed better to me).

Loirooriol avatar Apr 19 '23 18:04 Loirooriol

Right, I argued in previous discussions that forbidding decls after rules is fundamentally problematic. The basic question is "when has a rule occurred?", and this needs to not be dependent on whether a rule is valid or not, as that would mean different browser levels would interpret following properties differently.

So you need to define some notion of when we've found a rule, that allows for invalid rules, and do so in a way that doesn't unduly restrict our future syntax options.

For example, if we ever allow {} in a property, and you write it invalidly so it triggers rule parsing, would that kick the "now there's a rule" switch? Does that mean we actually have to forbid ever using {} in properties?


On the other hand, sticking with the current design that just allows them, and relying on people to not write unreadable stylesheets, suffers from none of these issues.

tabatkins avatar Apr 19 '23 21:04 tabatkins

Not saying it's necessarily a good idea, but playing devil's advocate a bit, we already have that kind of concept for e.g. @import / @namespace / etc, don't we?

Imagine someone wrote this ten years ago:

:has(body) {}

@import "something.css";

(Or something along those lines)

Their @import would stop working on browsers that support :has(), yet that hasn't been ever a concern (and I don't think it should be).

The switch for that is "A valid rule has been parsed", and I think we could do the same, so that random garbage doesn't cause that action at a distance.

emilio avatar Apr 21 '23 14:04 emilio

I think these are pretty different situations in practice, tho. We add to the set of "things that can go before @import" super rarely -- in fact, we've only done it once, with @layer.

On the other hand, we add new things that would qualify for nesting fairly regularly - we recently added @scope, and just resolved to add @initial-name-tbd. So the set of things that may or may not trigger "no more properties from here on" is regularly changed, presenting new hazards.

Ultimately, the question is still - what are we trying to protect authors from? This sort of interleaving has been allowed in Sass and related tools for many years, with the exact same "pretend all the properties are together at the top" behavior, and never caused notable issues.

tabatkins avatar Apr 27 '23 17:04 tabatkins

For example, if we ever allow {} in a property, and you write it invalidly so it triggers rule parsing, would that kick the "now there's a rule" switch? Does that mean we actually have to forbid ever using {} in properties?

If you mean the property value and I interpret the algorithm correctly, then {} is already allowed by consuming a component value in step 5 of consuming a declaration. And it can't trigger rule parsing because the consumed component value is appended to the declaration's value.

Sebastian

SebastianZ avatar Apr 27 '23 19:04 SebastianZ

This is in reference to the resolved-on new parsing algo that tries to parse as a decl, then falls back to parsing as a rule if the result wasn't valid. That algo isn't in the Syntax spec quite yet.

tabatkins avatar Apr 27 '23 20:04 tabatkins

That algo isn't in the Syntax spec quite yet.

Should we expect more changes to come (in CSS Syntax 3, at least) after this commit?

color: red; @media { foo: bar } color: green does not seem to parse as I would expect against <declaration-list>, probably because there is no ; after foo: bar. I think } should be provided as a stop token to consume a declaration.

cdoublev avatar May 11 '23 06:05 cdoublev

Thanks for spotting that! I hadn't gotten around to updating my own parser to the new text to test it, so I missed that I was mishandling nested constructs.

I believe it's all good now; the three construct consumption algorithms (at-rule, qualified rule, declaration) now all take a "nested" bool, which triggers them to stop early when encountering a top-level unmatched }. This is passed by "parse a block's contents" (which is renamed from "parse a declaration list" since it definitely returns more than just declarations).

tabatkins avatar May 11 '23 21:05 tabatkins

<declaration-list> is defined with consume a list of declarations and the procedure to parse the input of CSSStyleDeclaration.cssText is defined with parse a list of declarations. Both procedures are removed.

I do not mind waiting for an update to your parser to validate your updates. This would prevent me from polluting this issue with implementation details.

That said, <declaration-list> could be renamed to <statement-list> and could be parsed with consume a list of statements.

This would allow using <declaration-list> for when a list of declarations (strict) is required (eg. keyframe rule, @font, etc). But I do not know if it would be backward compatible to apply it to existing rules, and your intent may even be to preserve this flexibility to accept declarations/rules within any rule, for future extensibility.

cdoublev avatar May 12 '23 12:05 cdoublev

I've done the parser update, but had to stop at end of day before I could get my testing framework updated to the new data structures. Later today I'll have it working. ^_^

But I do not know if it would be backward compatible to apply it to existing rules, and your intent may even be to preserve this flexibility to accept declarations/rules within any rule, for future extensibility.

Yes, all rules already handled at-rules in their blocks even if they only accepted declarations validly, so that's staying, and at that point there's not really any reason to continue having a parsing split. All blocks are parsed the exact same way now in Syntax, accepting all three kinds of constructs (at rules, qualified rules, declarations).

My plan on reshuffling the productions is just to define one generic production, and then probably some sub-productions that automatically imply certain restrictions on what's valid so you don't have to say it in prose. But they won't change the parsing behavior.

tabatkins avatar May 12 '23 17:05 tabatkins

Unfortunately, it appears that Chrome shipped before we could decide on this, and now authors are already blogging about the "gotcha".

Hopefully it's not too late to change this. I think we should really try to avoid any rewriting that changes order of declarations.

Since we’re already resolved that nested MQs will wrap their contents with an implicit & {}, perhaps we can do the same thing with any declarations after the first nested rule, as @emilio proposes in the OP?

LeaVerou avatar Jun 15 '23 20:06 LeaVerou

I still feel very strongly that we should not change this, and the current behavior is the best. Again, this is the exact behavior that Sass (and I suspect other preprocessors) have had for a decade+ already, and it has not been a problem there. (Largely because people just don't write that code - they put their declarations first, then their nested rules.) I don't think we should try to add more behavior for something that has proven itself to not be a problem in practice.

MQs wrap all naked properties in an & {}; style rules obviously cannot do this, so the behavior would be inconsistent either way. As the spec is currently written, style rules and MQs are each internally consistent with a single behavior for all naked properties. I think it would be a (probably minor) bad thing for style rules to have two behaviors, depending on relative ordering of rules and declarations.

tabatkins avatar Jun 15 '23 22:06 tabatkins

While I would like to see a world where the order would stay as written, I think I agree with Tab on that this is the behavior that was implemented in all preprocessors: less, sass and stylus, at least some CSS-in-JS solutions (tested in styled-components, couldn't find a good playground to share), and nested PostCSS plugin (can be tested here), lightningCSS (but it polyfills the current spec).

Given this was done literally everywhere, I'd say it is not worth it to change this.

kizu avatar Jun 16 '23 19:06 kizu

The CSS Working Group just discussed [css-cascade] [css-nesting] Figure out whether we're fine with "shifting up" bare declarations after rules.

The full IRC log of that discussion <bramus> topic: https://github.com/w3c/csswg-drafts/issues/8738
<bramus> emilio: right now in nesting, when you have mixed declearations and nested rules, the current behavior of nesting (and sass?) is to pull all the declarations up which may feel a bit weird if you dont know this happens
<bramus> … the source order changes
<bramus> … dont know where we ended up in this discussion
<bramus> … but i think it is unfortunate
<bramus> … if ppl are fine with no change, so am i
<bramus> Rossen_: tab’s last comments were suggesting that
<fremy> Oh, thank you @emilio for filing this
<bramus> TabAtkins: yes
<bramus> Rossen_: remarks?
<bramus> fremy: thank you emilio for filing.
<bramus> … would love if we could fix this but understand that implementations are shipped
<bramus> fantasai: i think this is very confusing. On the plus side it is not common authors will do this, where they have the same specificity as the ??? and they are modifying the same property. so authors will not commonly run into this
<bramus> … when they do, the fact that it applies out of order is very confusing
<bramus> … we dont have to have this problem
<bramus> … in terms of options, we can say that when you are interleaving, you have to make additional ampersand rules to have them maintain their position
<fremy> q+
<bramus> … seems stragithforward but are concerned that we should fix the cascade bc it is confusing
<miriam> q-
<bramus> TabAtkins: main arg for no change is that this behavior is the same as what preprocessors do since they supported it
<bramus> … it has not been a problem for any of them as far as we can tell
<bramus> … for sass we have a maintainer confirming that
<bramus> … so we dont need to worry about it I think
<bramus> … it is not an issue in practice, so we should not come up with complicated solution
<bramus> … MQs and non-style rules have a different behavior when nested than style rules are
<bramus> … style rules put all their props in the style as ??? if it were up front
<myles> q?
<bramus> s/???/
<bramus> … and mqs in similar put all their ?? in nested style rules
<Rossen_> ack TabAtkins
<bramus> … we could run into the issue that knowing when nesting has begun depends on non; it is not ituitive what to define well when we have started nested
<bramus> … a rule that is unknown or mistyped can change that interpretation
<bramus> … in an unpredictable way
<bramus> …. to keep consistent behavior lets avoid that problem entirely, and data from preprocessors has shown it is not a problem
<bramus> fremy: the last argument seems valid
<Rossen_> ack fremy
<bramus> … the examples emilio gave is not about nesting selectors but nesting an at-media rule, which I can do
<bramus> … i feel example is convincing enough
<bramus> … is implementation of sass/less supporting this use case?
<bramus> … if they dont, then we are making things quite ???
<bramus> TabAtkins: yes, i believe they do
<bramus> … throw it into a sass playground and it will output the euiqvalent compliled code with media shifted out
<bramus> … the two decls will be grouped together
<Rossen_> q?
<bramus> s/grouped/combined
<bramus> fremy: in this case I dont feel strongly enough that we should
<bramus> emilio: so fixing interleaved decls inside a grouping rule by wrapping in &-rule is easy
<bramus> … fixing style rules by adding nested style rules is not too hard but … I guess we could fix it
<bramus> … dont feel too strongly either way
<TabAtkins> yup, today emilio's examples compiles to "the div with both properties, followed by the @media holding a div holding the conditional props"
<ntim_> q+
<bramus> Rossen_: so then the proposed resolution is to close no change
<ntim_> q-
<bramus> fremy: did anybody look into ???’s comment?
<bramus> Rossen_: we did,
<myles> q?
<fremy> s/???/Lea Verou/
<bramus> jensimmons: we do not like the proposed resolution. it does not make sense for authors right now. they expect for the later styles to apply
<bramus> … i agree that a non-complicated thing is a goal, bu tshould not be what we have now
<ntim_> +1
<bramus> … sass is a guiding principle, but we should follow how CSS has always worked: source order
<bramus> … ntim has a proposal
<bramus> ntim_: emilio wrote it in the issue : wrapping in &
<miriam> Here's the example in a Sass playground, for reference (sorry it's a long url): https://sass-lang.com/playground/#MTFkaXYlMjAlN0IlMEElMjAlMjBjb2xvciUzQSUyMGdyZWVuJTNCJTBBJTIwJTIwJTQwbWVkaWElMjAod2lkdGglMjAlM0UlMjAwKSUyMCU3QiUwQSUyMCUyMCUyMCUyMGNvbG9yJTNBJTIwcmVkJTNCJTBBJTIwJTIwJTIwJTIwYmFja2dyb3VuZCUzQSUyMHJlZCUzQiUwQSUyMCUyMCU3RCUwQSUyMCUyMGJhY2tncm91bmQlM0ElMjBncmVlbiUzQiUwQSU3RA==
<bramus> astearns: just for my clarification: that solution will allow later rule to override the earlier ones?
<bramus> [multiple]: yes
<bramus> plinss: i presume for wrapping ??? in another declaration block it becomes another rule in the OM?
<bramus> TabAtkins: yes
<bramus> plinss: is the & still required these days?
<bramus> TabAtkins: yes, you need a selector
<bramus> … in terms of raw css syntax parsing, omitting ths eelector will give you a rule with empty prelude and that will fail to parse as valid style rule
<oriol> q+
<bramus> TabAtkins: further issue as explained in thread. anything we do that distinguished behavior on before/after nested rule means we have to define the switch for 'we are past a nested rule'
<emilio> q+
<bramus> … cant be ??? to cause the syntax trigger
<bramus> … we dont need that concept if we dont do this
<bramus> … it will potentially change the cascade
<bramus> emilio: why would you have problem to just use valid nested rule as trigger?
<bramus> TabAtkins: bc a new rule that does not exist in older browser that understands nesting will throw away tgha tnew rule and conclude tha tnesting hasnt started yet
<bramus> emilio: and the declarations will ?? and that seems fine
<bramus> TabAtkins: and the OM changes
<bramus> emilio: ????
<bramus> emilio: like the OM changes, but it also changes when you itnroduce new rules.
<bramus> … you end up with different ??? list
<Rossen_> q?
<bramus> fantasai: if we have style rule with interleaved rules and declarations. lets say 3 decls at top, at-rule, and 2 decl at bottom
<SebastianZ> s/???/length/
<bramus> … old browser will have 5 decls appear in .style of that stylerule
<bramus> … if we wrap it in ??? then in a new browser you would get first 3 decls in ./style. then at-media in .cssRules followed by - followed in it - a new style rule with last 2 decls
<bramus> TabAtkins: not, but that is closely related.
<dbaron> ScribeNick: dbaron
<SebastianZ> s/???/an :is() rule/
<Rossen_> ack oriol
<TabAtkins> specifically, `::before { & {color: red;}}` does *not* apply red to ::before in today's spec
<dbaron> oriol: about wrapping decls in an &: with two elements, the & can't represent 2 elements. If you have a declaration, garbage, and a declaration -- if later a new feature parses the garbage as a nested rule, then the following declaration would be grabbed and not work.
<TabAtkins> and causing implicit wrapping would trigger that problem as well
<matthieudubet> (is oriol mic working ? the sounds seems far away)
<dbaron> oriol: while I agree the current behavior is not great, and would be a good oargument for chosoing option 4, I'm leaning more towards what tab is saying -- keeping current behavior
<emilio> ack emilio
<Rossen_> q?
<matthieudubet> (yes that was better at the end with the new mic)
<dbaron> TabAtkins: I forgot about that -- I agree that kills it harder. You can't nest if your parent rule has pseudo-elements. That's the behavior of :is(). If you tried to do this and parent selector applied styles to pseudo-elements, the implicit wrapping would just drop these declarations on the floor. That's broken at a fundamental model level right now.
<dbaron> plinss: that goes away if we get rid of the :is() behavior, right?
<dbaron> TabAtkins: yes
<ntim_> q+
<dbaron> TabAtkins: my proposed resolution is still to close with no change; I think current spec is right.
<dbaron> TabAtkins: but we should see if objection from Apple is still standing
<Rossen_> ack ntim_
<dbaron> ntim_: I wonderif we can translate to a ? rule without using &, just using the selector text.
<ntim_> div { color: green; } @media (width > 0) { div { color: red; background: red; } } div { background: green }
<dbaron> TabAtkins: no, b/c that's a relative selector
<dbaron> TabAtkins: those nested rules are "div div"; it's still relative to the parent rule
<dbaron> hober: siblings, not nested
<dbaron> myles: there's 3 sibling rules in that example
<dbaron> TabAtkins: that would mean treating nesting as a preprocessor directive that rewrites into a different rule structure
<dbaron> TabAtkins: we haven't been pursuing that approach since very early on in nesting. I'd like to reject trying to rewrite style things. That makes many things more complicated, like for how @media nesting works in rules. But the more we diverge the OM and the model underneath from the syntax authors write, the worse it is.
<dbaron> fantasai: I think I'm not comfortable resolving on no change -- it doesn't sound like we have consensus -- but we don't have a clear counterproposal worked out. I think TabAtkins brought up many useful concerns with emilio's path. We should take some time outside the meeting to review minutes and try to think through the psosibilities. Might be down to these 2, but maybe something else. And see if we can address relevant concerns, or at
<dbaron> ... least have a better understanding of what they are
<dbaron> [mic out of battery]
<jensimmons> +1 to what Elika just said
<dbaron> TabAtkins: If you think it's valuable to more exploration, you're welcome to. I don' t think it's going to work, but you're welcome to.
<dbaron> fantasai: Sounds like there's an aciton item for people with concerns about current proposal to come up with a counter proposal.
<dbaron> Rossen_: No rule that we can't reopen issues.
<dbaron> fantasai: I object to resolving when this many people don't like the direction.
<dbaron> TabAtkins: As long as it's not a drastic change (like turning it into a rewriting rule), unlikely there will be compat concerns if it changes relatively soon, given that this is a rare code pattern. But not indefinitiely, and limitations on how far that reaches.
<dbaron> Rossen_: So let's move on to the next issue. And once Apple or anyone else has a better proposal, bring it back.
<astearns> fwiw I have already seen authoring advice against mixing rules and declarations this way, so if people follow that advice it lessens the compat concerns

css-meeting-bot avatar Jul 19 '23 22:07 css-meeting-bot

I thought it was very odd that the WebKit poll seems to be favoring Option 1 (shifting up), so I posted a couple more polls:

which so far seem to be showing a very different picture with more than 3 out of 4 expecting no shifting. There are even people incorrectly explaining what happens (1 2)

For the minority that expects the current behavior, it often seems to be due to misconceptions around how specificity works (1 2 3 4 5). Others mentioned that while not shifting feels more natural, shifting has been beaten into them by existing implementations (e.g. 1 2 3 ).

There is also this older poll but it's phrased as a quiz, which influences the data, as people expect the result to be weird. Even so, it shows an even split between shifting and no shifting.

I think the current behavior is extremely unintuitive, and 10 years down the line we will regret opting for consistency with today’s preprocessors over predictable, natural behavior. In fact, we even have a TAG principle advising against this exact thing: Prioritize usability over compatibility with third party tools. I'd even vote for entirely disallowing declarations after rules over the current behavior, as it would give us more time to figure this out.

I’m quite concerned that wording seems to influence the result so much, as it indicates we don't yet have a good picture of author expectations. Perhaps we need more data here. Maybe an MDN short survey would help?

Agenda+ to resolve to temporarily disallow declarations after rules while we try to get better data here, so that we don't get stuck in a situation where we can't change the behavior due to web compat.

LeaVerou avatar Oct 04 '23 14:10 LeaVerou

FWIW we (WebKit) were also very surprised by our poll results (it actually changed over the weekend, the first days were Option 2 winning 70/30).

Disallowing declarations after rules seems the "worst" solution because it means we have to determine the "a rule have been parsed" trigger which could cause some compatibility issue if we extend what correctly parse as a rule in the future. However, I'm not sure what would be the risk if we allow declarations after rules like now and just follow the cascade so last one wins?

mdubet avatar Oct 04 '23 15:10 mdubet

Might be good to resurface/link the CSSOM aspects of this as I don't see any mention of those in this issue. If I recall correctly it was an issue for CSSOM if declarations and rules could be interleaved.

(maybe there is a clever way to resolve those?)

romainmenke avatar Oct 04 '23 15:10 romainmenke

Disallowing declarations after rules seems the "worst" solution because it means we have to determine the "a rule have been parsed" trigger which could cause some compatibility issue if we extend what correctly parse as a rule in the future.

Could you please elaborate on that? Not sure I follow what you mean by "a rule has been parsed trigger". FWIW I don't think disallowing declarations after rules is a good long-term solution; I'm only proposing it so that we don't back ourselves into a corner while we deliberate and time passes.

However, I'm not sure what would be the risk if we allow declarations after rules like now and just follow the cascade so last one wins?

I think the argument against that is inconsistency with preprocessors (and perhaps the existing Nesting implementations? Though usage of these in the wild right now is effectively nil, and even smaller where this changes things). Not sure if there's any other counterargument.

Might be good to resurface/link the CSSOM aspects of this as I don't see any mention of those in this issue. If I recall correctly it was an issue for CSSOM if declarations and rules could be interleaved.

(maybe there is a clever way to resolve those?)

From what I remember, we resolved that by resolving that bare declarations can be wrapped with & { ... }, though I can't find that right now.

LeaVerou avatar Oct 04 '23 15:10 LeaVerou

Could you please elaborate on that? Not sure I follow what you mean by "a rule has been parsed trigger".

Yeah, I've elaborated on this in the past when Emilio suggested disallowing properties after rules.

So, this is a parsing switch. In theory parsing switches are doable; we explored a few of these earlier when working thru Nesting options. But the switch needs to be reliable - the earlier discussion about @nest; being the switch worked, because we know exactly what to look for and don't expect that to change in the future.

But if the parsing switch is "a rule of some kind is seen", that's hard. The most obvious interpretation of that is "a valid rule of some kind is seen" - that's well-defined, but it means that authors can see unexpected differences in parsing behavior if the rule in question is supported in some browsers but not others, as older browsers will throw out the rule and continue allowing declarations, while newer browsers will see it and disallow declarations. (And consider: an invalid selector makes the style rule invalid, and we do new selectors all the time.)

So ideally we define invalid rules as also triggering this. But then we open up the full syntax space of what an "invalid rule" actually is. Is foo bar:baz {...}; an invalid rule? Or is it a new property syntax? We can define something for this, but it means restricting our future evolution capabilities, to a larger extent than what we've already done for Nesting.

tabatkins avatar Oct 04 '23 21:10 tabatkins

FWIW we (WebKit) were also very surprised by our poll results (it actually changed over the weekend, the first days were Option 2 winning 70/30).

Have the results been restricted? I remember seing opposite numbers yesterday.

b-strauss avatar Oct 05 '23 08:10 b-strauss

I watched the results of the poll very carefully in the first couple days. It was consistently 37-39% for Option 1, and 61-63% for Option 2.

It's very telling when you hit enough votes (like 50 or 100) and the results stabilize. As more and more votes came in, the results did not change.

Sept 28 at 2:50pm ET. Screenshot 2023-09-28 at 2 50 18 PM

Sept 29 at 12:50pm ET. Screenshot 2023-09-29 at 12 50 02 PM

Then over the weekend, another 1800 votes came in, with an overwhelming preference for Option 1. Almost three times as many votes came in over the weekend? Long after we stopped promoting the article on social media? With a radically different result?

That's a bot.

So we decided to close the survey and post the last known results from before the traffic pattern became highly suspect.

You can see a similar preference for Option 2 in Lea's survey: https://mastodon.social/@[email protected]/111177156433448874

jensimmons avatar Oct 05 '23 21:10 jensimmons

I agree with Lea:

I think the current behavior is extremely unintuitive, and 10 years down the line we will regret opting for consistency with today’s preprocessors over predictable, natural behavior.

A desire to match Sass is a terrible reason. (Especially if the only reason Sass & other tools made their choice is that they could not implement the more intuitive behavior.)

We should be designing the language for the future — for 20+ years from now, when the majority of developers have never used Sass, and those that did don't remember how it worked.

I've not heard any other reason put forth besides: this is how all the third-party tooling does it, and we don't think it's a big deal. I haven't heard anyone arguing that this design is good for the cascade, or makes sense, or is something future developers will easily learn.

I believe this will be a very confusing problem for developers trying to debug their code if we don't fix it.

jensimmons avatar Oct 05 '23 21:10 jensimmons

I think I was initially for option 1, but gradually moved towards option 2.

Mainly, deciding for me are the “designing the language for the future” combined with the case being very rare and usually considered a bad practice by itself. If it was something that is present in all current preprocessors, but very commonly used it would be a different story, but this is a case of an edge-case, where preprocessors decided to interop for some reason.

I think it should be ok to handle this edge-case differently, but properly.

kizu avatar Oct 05 '23 21:10 kizu

Well said @jensimmons. I would urge anyone who wants to weigh in on this to also read the responses to both of the polls I posted. They are even more illuminating than the (sweeping) quantitative data.

LeaVerou avatar Oct 06 '23 05:10 LeaVerou

I do not believe it's correct for these two blocks to end up with different results:

h1 {
  color: yellow;
  @media (width > 0) {
    color: red;
  }
  color: green;
}
h2 {
  color: yellow;
  @media (width > 0) {
    color: red;
  }
  & {
    color: green;
  }
}

(Try it in a browser with support for CSS Nesting: https://codepen.io/jensimmons/pen/KKbrJBp/5861d875920bb25695c12975bf627b75?editors=1100 )

The ampersand should be a clean substitute for the unnested selector, not something that changes the result.

jensimmons avatar Oct 06 '23 17:10 jensimmons

I've used Less a lot, and have been using Sass for over 6 years now on a huge strictly Sass code base, and I've never stumbled on this behavior, I'm actually surprised by it. I guess everyone have been writing declarations before rules everywhere.

So IMHO this is also counter intuitive for most devs whom are using preprocessors, that expect the expected CSS behavior of "latter wins".

ydaniv avatar Oct 06 '23 20:10 ydaniv

I think it's clear that the current behavior is quite bad because it breaks the principle of "last declaration (with same specificity) wins", I'd have objected against option 3 if I had realized this at the beginning. But at this point we are stuck with that syntax, and trying to address this problem within option 3 seems to cause even worse outcomes, so probably we will have to live with it. Could be added to the list of CSS mistakes.

Loirooriol avatar Oct 16 '23 12:10 Loirooriol

(Especially if the only reason Sass & other tools made their choice is that they could not implement the more intuitive behavior.)

They absolutely could have implemented either behavior; it's just outputting a separate rule rather than combining into one rule. They do exactly that if you do wrap the latter declaration in a & {...} rule, so both behaviors are clearly possible.

I do not believe it's correct for these two blocks to end up with different results:

I think the consistency argument is reasonable in either direction. Before nesting, if you wrote:

h1 {
  color: yellow;
  color: green;
}

you'd get a single 'color' declaration with the value green. One can reasonably argue, I think, that it's also consistent that adding an unrelated rule (the @media in your example) shouldn't change the behavior of these declarations. I suspect that might be why the preprocessors originally chose the behavior that they did. Adding an explicit & {...} wrapper around the latter declaration is a much stronger declaration of intent than just inserting an unrelated rule before it.

We should be designing the language for the future — for 20+ years from now, when the majority of developers have never used Sass, and those that did don't remember how it worked. ... I believe this will be a very confusing problem for developers trying to debug their code if we don't fix it.

Do we have any evidence that this is actually confusing to users of Sass, Less, or any other preprocessor? So far all I've seen is people arguing that, now that it's been pointed out to them, it's kinda confusing; I haven't seen any evidence so far that this is actually confusing developers in the wild, despite over a decade of usage and millions of users.

(I'm not disputing that there might be such evidence, I simply haven't seen any.)


I have no strong opinion on which way we go for this. But the fact that the current spec is the behavior of essentially every preprocessor, and afaict there have been approximately zero complaints about it for over a decade of use (because, again afaict, nobody actually writes code like that in the first place), means that there's very little reason for us to care about what the behavior is either. As such I'd prefer no change, as compatibility with the wider ecosystem is a (relatively minor) benefit, but I won't object over the rest of the WG if the decision goes the other way.

tabatkins avatar Oct 17 '23 01:10 tabatkins

@tabatkins

If preprocessor users haven’t really stumbled on this in the first place, compatibility with preprocessors is not a benefit, minor or otherwise. It's only a benefit when it's compatible with behaviors they have, actually, experienced. Adding something to CSS is a much wider deployment than adding it to a preprocessor, so “people haven't hit this problem before” should not be an excuse for weird behavior.

If you read through the thread and the various polls, there is a very strong signal from developers that the current behavior is confusing. Even worse, for the few that don't find it confusing, it’s due to a broken mental model about the cascade: they thought that @media adds specificity. So I’m quite worried not just about the ergonomics of this, but also what it teaches authors about the rest of CSS.

And it's not like there's an actual implementation reason for the confusing behavior, right? It seems we all agree (?) that changing it produces better ergonomics. So what's the argument for keeping it? Compatibility with Sass and co? We literally have a TAG principle about this exact thing: 2.12 Prioritize usability over compatibility with third-party tools.

LeaVerou avatar Oct 17 '23 13:10 LeaVerou

An argument for keeping it could be performance, mostly. E.g., if you do something like:

.foo {
  --bar: baz;
  @media (a) {
    --bar: something-else;
  }

  --baz: ...;
  @media (b) {
    --baz: something-else;
  }

  // Repeat x100 etc
}

If we generate a bunch of split rules for anything after an @media rule, that can cause useless overhead, which is also surprising.

emilio avatar Oct 17 '23 14:10 emilio