w3c / csswg-drafts

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

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

Closed emilio closed 4 months ago

emilio commented 1 year ago

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:

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

Maybe something else?

cc @fantasai

Loirooriol commented 1 year ago

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).

tabatkins commented 1 year ago

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.

emilio commented 1 year ago

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.

tabatkins commented 1 year ago

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.

SebastianZ commented 1 year ago

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

tabatkins commented 1 year ago

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.

cdoublev commented 1 year ago

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.

tabatkins commented 1 year ago

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).

cdoublev commented 1 year ago

<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.

tabatkins commented 1 year ago

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.

LeaVerou commented 1 year ago

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?

tabatkins commented 1 year ago

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.

kizu commented 1 year ago

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.

css-meeting-bot commented 1 year ago

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
LeaVerou commented 11 months ago

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.

mdubet commented 11 months ago

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?

romainmenke commented 11 months ago

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?)

LeaVerou commented 11 months ago

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.

tabatkins commented 11 months ago

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.

b-strauss commented 11 months ago

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.

jensimmons commented 11 months ago

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/@leaverou@front-end.social/111177156433448874

jensimmons commented 11 months ago

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.

kizu commented 11 months ago

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.

LeaVerou commented 11 months ago

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.

jensimmons commented 11 months ago

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.

ydaniv commented 11 months ago

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".

Loirooriol commented 10 months ago

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.

tabatkins commented 10 months ago

(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.

LeaVerou commented 10 months ago

@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.

emilio commented 10 months ago

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.

mdubet commented 10 months ago

Implementation wise, I think it's a bit more complicated to implement the "last declaration (after rules) wins" if we assume the most obvious way : wrap following declarations in & { }

<style>
.foo::before {
  background-color: blue;
  content: 'nonest';
}
.foo::before {
 & {
  background-color: green;
  content: 'nest';
  }
}
</style>

<div class=foo>Foo</div>

There might be a better way to implement all this though.

LeaVerou commented 10 months ago

@emilio

An argument for keeping it could be performance, mostly. E.g., if you do something like: [snip] If we generate a bunch of split rules for anything after an @media rule, that can cause useless overhead, which is also surprising.

Given that this doesn’t happen much, I don’t think that would be significant in practice. Also, I suspect when people write code that way (with a declaration, and then a MQ after it to set just that property), the MQs are not that varied, so a low-hanging optimization would be to merge them together when it doesn’t change the outcome. Meaning:

.foo {
    grid-template-columns: auto 1fr auto;
    @media (width < 500px) {
        grid-template-columns: auto 1fr;
    }

    gap: .5em;
    @media (width < 500px) {
        gap: .3em;
    }
}

becomes:

.foo {
    grid-template-columns: auto 1fr auto;
    gap: .5em;

    @media (width < 500px) {
        grid-template-columns: auto 1fr;
        gap: .3em;
    }
}

This is also consistent with our rule to serialize to the shortest equivalent syntax.

@mdubet

the serialisation will generally be more verbose

Not if we go the other way and decide to serialize all & {}s after rules as bare declarations. Which would also be consistent with our rule to favor the shortest equivalent syntax on serialization. This would also be more likely to preserve author intent, since I bet most & {} rules would have come from wrapping bare declarations, rather than specified explicitly by the author.

emilio commented 10 months ago

I don't think we have a precedent for rewriting / merging rules, and I don't think we'd want to add such thing.

LeaVerou commented 10 months ago

I don't think we have a precedent for rewriting / merging rules, and I don't think we'd want to add such thing.

Wrapping declarations in & {} is also technically rewriting, so I think that ship may have sailed?

mirisuzanne commented 10 months ago

I wonder if we should consider how this behavior relates to similar future features, e.g. mixins, if we eventually go down that path. For example, an @apply --x-mixin; (or similar) rule could be treated as an extension of nesting – a placeholder for & { <output of --x-mixin> }. With mixins it is more clearly important that authors have an easy way to override the output by providing additional declarations after the mixin. In fact, most Sass 'best practices' have encouraged putting mixins before declarations rather than after.

Clearly, nesting and mixins wouldn't need to behave the same (they don't in Sass), but it seems like a useful comparison to consider? Even if we don't use the same solution for both, mixins are likely to raise the same issue.

css-meeting-bot commented 10 months ago

The CSS Working Group just discussed [css-cascade] [css-nesting] Figure out whether we're fine with "shifting up" bare declarations after rules, and agreed to the following:

The full IRC log of that discussion <fantasai> lea: Right now, if we have bare declarations after a nested rule, current behavior is that they're shifted up to be before the nested rule
<fantasai> ... which means nested rule can override them
<fantasai> ... [gives example in the issue]
<astearns> q?
<fantasai> ... This behavior violates source order when specificity is the same, and is against author expectations
<lea> https://github.com/w3c/csswg-drafts/issues/8738
<fantasai> ... WebKit ran a poll showing that
<fantasai> ... I ran two polls, here's link to both of them
<fantasai> ... Almost 80% of authors were expecting the different behavior
<fantasai> ... and those who didn't seemed to have a broken mental model around specificity
<fantasai> ... things like "@supports increases specificity"
<chris> that seems surprising, it breaks document order. I bet people spend time debugging this
<fantasai> ... That's quite dangerous, not just that this feature has a weird behavior, but it encourages a broken mental model of how CSS works
<fantasai> ... specificity is already confusing as it is, we don't need to add to that
<fantasai> lea: in terms of changing behavior, not really much reason
<fantasai> ... in the thread it's "this is how preprocessors work"
<fantasai> lea: in the TAG we have a principle against this
<fantasai> lea: other option is compatible with existing tools
<fantasai> lea: we suggest that ppl go with the usability benefit
<fantasai> lea: 10 years down the line, how preprocessors of today work will not matter
<fantasai> lea: There was also point that maybe slightly easier to implement current way
<jensimmons> q+
<oriol> q+
<emilio> q+
<fantasai> lea: but doesn't seem that there is serious implementation complexity problem with changing to match source order
<fantasai> lea: Also, jensimmons ran several more polls, which validated the same point that authors find the behavior confusing
<astearns> ack jensimmons
<fantasai> jensimmons: I feel pretty strongly that we should change the behavior so that it matches what people expect from CSS cascade, that later thing overrides earlier thing when specificity is tied
<fantasai> jensimmons: People will not remember what SASS did 20 years from now
<astearns> ack oriol
<fantasai> oriol: I think Lea posted the wrong link to the poll above...
<miriam> q+
<fantasai> oriol: I agree that we should not prioritize aligning with preprocessors, since preprocessors can always change by releasing a new version
<fantasai> oriol: authors can adopt when they want
<lea> comment with polls: https://github.com/w3c/csswg-drafts/issues/8738#issuecomment-1746990112
<fantasai> oriol: but this wasn't the main reason
<fantasai> oriol: The question is how do you handle this?
<fantasai> oriol: One was that we would just ignore declarations after nested rules
<lea> q?
<fantasai> oriol: but that seems problematic if they add garbage, and could cause declarations to stop working, so concerned about future-compat
<fantasai> oriol: Wrapping in & doesn't work with pseudo-elements
<fantasai> oriol: so I don't think this is straightforward
<fantasai> oriol: While I agree that this is pretty bad and very confusing, and I would have objected to current syntax of nesting if I had realized this
<fantasai> oriol: I think alternatives seem worse
<fantasai> oriol: I don't see a clear proposal addressing all the concerns
<fantasai> oriol: I think the current state is the lesser of the evils
<lea> q+
<chris> unclear why current state is "worse"
<fantasai> astearns: I don't believe we're talking about discarding things after nested rules, only talking about how to keep them in and do the source order correctly
<fantasai> astearns: whether wrapping in an & is the way forward, would need to work through the issues with pseudos etc.
<astearns> ack emilio
<fantasai> astearns: Let's go back to the queue
<fantasai> emilio: I also brought another concern which may or may not be an issue
<fantasai> emilio: this also assumes we go with wrapping-in-&, which seemed like most straightforward solution
<fantasai> emilio: Wrapping stuff in extra rules can have surprising consequences perf-wise
<fantasai> emilio: you triple the amount of selectors that element has to match
<fantasai> emilio: I agree with Oriol that given the & stuff doesn't work with pseudo-elements, we either need to figure out how to fix that
<fantasai> emilio: but current behavior is better than having some declarations work and others not
<fantasai> emilio: we want to make sure all the declarations work
<fantasai> emilio: but then the nested at-rule would also not work?
<fantasai> emilio: so maybe pseudo-elements issue isn't such a great argument, since the at-rule inside would also be broken
<fantasai> astearns: not sure I follow
<fantasai> emilio: You have sth::before { declarations; @rule {... }; more-declarations; }
<fantasai> emilio: The issue is that more-declarations gets shifted up above @rule
<fantasai> emilio: but wrapping in & doesn't work, because :is() doesn't work with pseudo-elements
<fantasai> emilio: but actually you have this same problem with the at-rule
<fantasai> emilio: if you put bare declarations in the @rule, they also get magically wrapped i n&
<fantasai> emilio: so maybe wrapping in & isn't so terrible
<fantasai> emilio: it's weird if ones before work and ones after don't
<fantasai> emilio: but the @rule declarations would also be broken
<astearns> ack miriam
<fantasai> miriam: Not a lot to add, except to say that I agree it's confusing
<fantasai> miriam: I don't know why SASS did it in the first place
<fantasai> miriam: Authors develop convention of putting declarations first
<emilio> q
<fantasai> miriam: but that's not great
<emilio> q+
<fantasai> miriam: we're going to have similar problems with mix-ins in the future
<astearns> ack lea
<fantasai> miriam: be nice to not have a totally different solution
<fantasai> lea: Are we sure it would be a problem with pseudo-elements?
<fantasai> lea: there's no reason to use :is() for a single &
<fantasai> lea: You should just get same selector as parent. If you write something where that substitution isn't fine then you have a problem
<fantasai> lea: [missed]
<fantasai> lea: would enable media queries inside pseudo-elements, which seems important to allow
<astearns> ack emilio
<fantasai> emilio: Using :is() is kinda necessary in the sense that it allows you to avoid expansion, which prevents combnatorial explosion
<astearns> q+
<fantasai> emilio: but maybe special-casing the single & is OK?
<fantasai> emilio: but I suggest we first fix this, at least the bare at-rule issue and then
<lea> s/[missed]/If we don't already do that then we should, even aside from this issue/
<fantasai> emilio: figure out if we need to do this
<fantasai> emilio: It is observable due to specificity, right? This would get the combined specificity of all the items in the selector list
<fantasai> emilio: if you have #foo, bar { .. } then things inside at-rule would be different specificity
<fantasai> emilio: it would be weird to diverge
<fantasai> emilio: it would be weird to make & work differently than in any other position
<fantasai> ntim: :is() will take the higheste specificity
<lea> what emilio is saying is that .foo, #bar { & { color: green } } rn has different specificity than .foo, #bar { color: green } because it's rewritten as :is(.foo, #bar) { color: green }
<fantasai> emilio: which is why it's different. & will get the ID's specificity, even though that matches .foo rather than #bar
<lea> q?
<fantasai> emilio: assuming of course only one of the selectors matches
<fantasai> astearns: One of the arguments against making this change is that people use SASS conventions and nobody puts declarations after nesting anyway
<fantasai> astearns: anecdotally, when I went to search, the 2nd CSS file I looked at had bare declarations after nesting rule for SASS
<fantasai> astearns: probably an accident, but don't think you can say it's not done
<astearns> ack fantasai
<astearns> ack astearns
<emilio> fantasai: couple things emilio brought up and I think are insightful. First is that this is a current problem for @media in pseudo-elements
<lea> +1 to fantasai
<emilio> ... so we need to make it work with nested at rules
<emilio> ... second is that it's weird that that flattening of specificity is very weird and unexpected
<jensimmons> q+
<emilio> ... so I think we should address both of these issues
<emilio> ... but the fact that pseudo-elements are already a concern it means we need to address it
<emilio> ... but it shouldn't block us from solving the cascade
<emilio> ... proposal is that we agree that we're going to fix this
<emilio> ... and try to figure out a right way to fix it
<emilio> ... we could just make that & matches pseudos or something
<emilio> ... or we could come up with another symbol that matches pseudo-elements
<emilio> q+
<astearns> ack jensimmons
<fantasai> jensimmons: Adding to astearns, idea that people will organize their code properly, might have worked out a decade ago when sites were new and got overhauled every few years, and only a few developers
<fantasai> jensimmons: but now teams are much more massive, and code lasts much longer, 5-10 years
<fantasai> jensimmons: and people are scared to reorder styles, touching cascade could break things on other pages
<fantasai> jensimmons: they just jam things in, and might not understand that they're adding things before or after
<astearns> ack emilio
<fantasai> jensimmons: so industry has changed enough that I don't think relying on good CSS organization is workable
<fantasai> emilio: I agree with fantasai that we should bring up those issues and find a coherent way of fixing it
<bradk> +1 to @jensimmons
<lea> q?
<fantasai> emilio: the most striaghtforward solution would be to make & expand to the selector list when it's alone
<lea> q+
<fantasai> emilio: it would solve all the relevant issues here: would make pseudos work, would make @media inside stuff work, and it would make declarations wrapped in & work
<chris> also +1 to @jensimmons this makes css fragile and unmaintainable
<astearns> zakim, close queue
<Zakim> ok, astearns, the speaker queue is closed
<fantasai> emilio: it feels a bit inconsistent, but I think it's the only reasonable solution
<astearns> ack lea
<fantasai> lea: Agree with emilio. even if we decide this isn't the best solution, can invent a better one
<fantasai> lea: but saying that & alone should invoke the selector directly is a good optimization regardles
<fantasai> lea: no reason to have :is() there
<fantasai> lea: good change, solves a bunch of issues, and gives us time to solve the problems down the line
<argyle> i thought :is() only wrapped & when it was a selector list, but that changed somewhere along the line
<jensimmons> +1
<fantasai> lea: and means implementations can update, and that avoids web compat lockdown of waiting
<lea> argyle:
<fantasai> astearns:OK. Let's first resolve that we want to fix this issue, that we want the cascade to use source order
<fantasai> astearns: I haven't heard any arguments against this change, just details
<bradk> +1
<fantasai> RESOLVED: We will address ths issue, and fix nesting to allow for bare declarations after nested rules without moving them above.
<fantasai> astearns: unclear if there's one or two extra issues, can a volunteer untangle?
<fantasai> fantasai: The two issues are, pseudo-elements aren't handled properly by & because it wraps in :is(); and specificity is badly handled because & is wrapped in :is()
<fantasai> lea: should discuss together as one issue
<fantasai> astearns: OK, we're out of time, let's restart this conversation next week
<astearns> s/let's restart this conversation next week//
tabatkins commented 10 months ago

@leaverou

If preprocessor users haven’t really stumbled on this in the first place, compatibility with preprocessors is not a benefit, minor or otherwise.

Right, but my point is that neither behavior is a benefit, according to over a decade of experience. This simply does not matter to authors, as far as we can tell. So the "benefit to authors" part of the PoC is approximately 0; at best it's a learnability benefit, but better learnability for a case that doesn't appear to ever happen in practice is worth extremely little. So impl benefits, even minor ones, can weigh sufficiently here to sway the outcome.

a low-hanging optimization would be to merge them together when it doesn’t change the outcome.

We cannot do this. What things are raw decls and what are rules is observable in the OM. I absolutely do not want the OM to be dependent on "when it doesn't change the outcome".

Wrapping declarations in & {} is also technically rewriting, so I think that ship may have sailed?

No, it's not at all rewriting, in the sense that you're suggesting. How we interpret things on the first pass is up to us; you're talking about a completely different thing where something would be wrapped in a rule or not depending on the exact property names used across/between other rules. (Whether or not the first @media in your example used 'gap' would affect whether it "changes the outcome"!) That's an extremely non-local and non-obvious effect, and I'd object strongly to it.

Not if we go the other way and decide to serialize all & {}s after rules as bare declarations. Which would also be consistent with our rule to favor the shortest equivalent syntax on serialization.

We can't do this either, because it would mean that two consecutive &{...} blocks would roundtrip into a single one. And I don't think we want to specify that the serialization differs based on whether there's a single or multiple blocks with a particular selector in a row.

@miriamsuzanne

With mixins it is more clearly important that authors have an easy way to override the output by providing additional declarations after the mixin. In fact, most Sass 'best practices' have encouraged putting mixins before declarations rather than after.

This is a fair argument, and does argue for a real author benefit in doing the rule-wrapping. Sass gets away with it because it's just doing direct substitution of the @include with its contents.

Note that Sass's behavior is inconsistent in this regard; for example:

@mixin reset-list {
    & {
        margin: 0;
        padding: 0;
        list-style: none;
    }
}

ul {
    @include reset-list;
    margin: 1em;
}

/* compiles to */

ul {
  margin: 1em;
}
ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

So the "trailing" margin: 1em gets put before the mixin in the cascade and will be overridden! (Defining the mixin with naked properties rather than a nested rule does do what you suggested, tho.)

css-meeting-bot commented 10 months ago

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> TabAtkins: we got a resolution last week?
<bramus> lea: yeah, to solve the issue
<bramus> … not sure if this issue should be on the agenda, prolly the new one 9492
<bramus> Rossen_: Is that one ready? dont see agenda+
<bramus> … we can discuss sth else
<bramus> lea: orig issue was on the agenda because impls are shipping
<bramus> … changes should be sooner than later in that case
<bramus> … TabAtkins made point that authors dont hit it frequently
<bramus> … so maybe we have more time?
<bramus> … changing shipped behavior tends to be difficult though
<bramus> … the longer we wait, the more work to change it later
<bramus> TabAtkins: at the end of the OG issue, interaction with future planned things like mixins does mean we prolly dont want to shift
<bramus> … I do agree this is worth fixing and should be fine to fix relatively quickly for reasons lea explained
<bramus> … we should figure out secondary issue but happy to stick with last weeks resolution that they get wrapped instead of lifted
<bramus> Rossen_: remaining Q then is if we discuss the new issue now or not?
<bramus> lea: the discussion died down after a few days and TabAtkins said he would reject … would be nice to start discussing
<lea> q?
<lea> q+
<oriol> +1 to Tab
<bramus> TabAtkins: I explained why immediately in preceding comment. we cant treat selectors in ?? fashion based on how they were referefenced in nested selector. inconsistent and confusing.
<Rossen_> ack lea
<bramus> lea: the way the :is() flattens specificity (largest one) was most reasonable one but it is a wart. the less we expose authors to it, the better
<TabAtkins> `.foo, #bar { & .baz {...} .baz & {...} }` <-- these two nested rules absolutely need to have the same specificity
<TabAtkins> And `.foo, #bar { & {...}}` should have the same specificity as the above, minus one class.
<bramus> … i dont think there are any use cases that actually require flattending spcfcty
<bramus> … the less authors hit that, the better
<oriol> q+
<bramus> … similar to generating a math expression: not including () when you dont need to
<bramus> TabAtkins: yes, but the wart exists
<fantasai> But `.foo, #bar { & { ... } }` should have the same specificity as `.foo, #bar { ... }` is the argument
<Rossen_> ack fantasai
<bramus> fantasai: I agree we should solve soon. the way specificity works with is(), definitely not expected behavior.
<bramus> … nesting should not cause selectors to get flattened
<bramus> … if you have a class and an id, they mathc based on what they match
<bramus> … as soon was you start nesting they get flattened … definitley not expected
<TabAtkins> (And note that it's absolutely too expensive to not flatten *in general*, this was well-established in previous Nesting discussion. It makes matching exponential-time.)
<bramus> … & foo and foo & should have same specficity
<lea> Indeed, even beyond shifting, the fact that `.foo, #foo { h1 { color: red } }` flattens specificity is *incredibly* confusing (and wouldn't be solved by not using `:is()` when not necessary)
<bramus> … .foo.bar and .foo, .bar ???
<fantasai> .foo, #bar { color: blue; @media print { background: yellow; } }
<lea> e.g. it's common to do things like `button, .button { &:hover { } }`, now `button:hover` has the same specificity as `.button:hover`!
<lea> q?
<bramus> fantasai: these decls should have same specificity when the match, but they dont right now
<bramus> oriol: I agree with tab. idea of treating lone & vs when used in bigger selector seems incosistent and confusing
<lea> q+
<bramus> … i am confused. the main probl is with pseudo-els which cant be matched by &, but discussion seems to be shifting to specificity
<TabAtkins> I had a proposal for just `@nest { ...props here }` that is semantically identical to `& {...}` but doesn't impose the restrictions.
<bramus> … i dont really see the flattening as a blocking thing
<bramus> … maybe i am missing something
<bramus> … seems necessary in complex cases of nesting
<bramus> … if you need to check all possible combinatorial expansion to know the specificity, then it seems problematic
<bramus> … main problem was with psuedo-els though. could be possible to have & also match pseudo-els
<bramus> …which would fix this
<Rossen_> ack lea
<bramus> lea: I wrote an example ajust above, wher authors can trigger this
<bramus> … button and .button
<bramus> … headings and classes of headings
<bramus> … common to style element names with class names
<TabAtkins> (I would also be fine with defining `&` can match pseudos, fwiw. That wouldn't solve the specificity flattening, tho, so I lean more towards a solution that does more, and leave `&` consistent with `:is()`.)
<TabAtkins> q+
<bramus> … right now with the button example, the button:hover would hav especificiy of .button:hover
<bramus> … @nest would only solve the issue with internal rewriting
<bramus> … even proposed solution with ??? that would also not fix the issue
<bramus> … i disagree with myself from last week
<bramus> … we should solve root cause
<bramus> … one idea: we introduced :is() to avoid combo explosion.
<emilio> q+
<bramus> … what if instead of wrapping with :is() we do some smarter rewriting that retains specificiyt
<Rossen_> ack TabAtkins
<bramus> TabAtkins: unforutnately the specifiity issue is not fixable
<lea> q?
<lea> qq+ to reply to TabAtkins
<bramus> … it depends on the length of th eseleector list and number of levels
<bramus> … but, i think we can solve being able to put naked props
<bramus> … after rules or inside @media et al
<bramus> … that should be solved
<bramus> … its not great today
<bramus> … even more wrapping will make it worse
<bramus> … ?? is too expensive in general case
<bramus> … flattening specificity also
<bramus> … even if authors often produce the same specifity the wont always
<bramus> … so the general is still as expensive
<Rossen_> ack lea
<Zakim> lea, you wanted to react to TabAtkins to reply to TabAtkins
<bramus> lea: i was proposing to drop is()
<bramus> … what if we do some smarter rewriting, to retain specificity?
<bramus> … it would be very productive to have an eexample of combinatorial explosion
<bramus> TabAtkins: nothing is forcing authors
<bramus> … example is make a selector list of n selectors and nest it a few times
<bradk> Just a thought, but maybe flattening ‘button, .button’ into the same specificity as .button isn’t necessarily a bad thing.
<bramus> … problem becomes exponential to resolve inner property
<Rossen_> ack emilio
<bramus> emilio: another potential solution would be to make is specificity dynamic, but we resolved against that in the past
<bramus> … you would still need to match all selectors
<bramus> … seems very non-triviail … like make the parent selector behave as dynamic specificity
<lea> it does introduce a new dependency for calculating specificity, where it depends on the element matched
<TabAtkins> `a, .a, #a { a, .a, #a { a, .a, #a { ...}}}` <- already 3^3 possible specificies
<bramus> … i seem to recall webkit implemented ?? but that also has a lot of problems
<emilio> s/??/:matches()/
<bramus> Rossen_: good discussion, and we are at time
<bramus> … lets end here and continue discussion in the issue
<bramus> … feel free to bring back next week when enough progress
<bramus> … enjoy the rest of your week
<bramus> topic-end
tobymackenzie commented 10 months ago

I was able to find a Sass issue desiring the no "shifting up" behavior from 2014. It is still open and tagged as "planned", though seemingly no work has been done on it since.

jabcreations commented 9 months ago

Why is this even a discussion? 🙄︀

Cascading ⇦ Style Sheets

Example 1, color used is green:

p
{
 color: red;
 @media (width > 0) {color: yellow;}
 color: green;
}

Example 2, color used is green:

p
{
 color: red;
 color: yellow;
}

 @media (width > 0) {p {color: green;}}

All the rules are at the same "level" with a simple p selector and lack !important.

vrubleg commented 8 months ago

The longer this ticket stays forgotten, the harder it will be to change in the future.

We should at least forbid declarations after nested rules until the final decision is done. Current behavior is confusing and can be considered as a mistake that will bother people forewer if not fixed.

andruud commented 8 months ago

@vrubleg Not forgotten, I'm investigating the feasibility of this. I had high hopes that we could make this change, but the use-counter I've added so far shows that ~0.16% of pageloads already place bare declarations after nested rules. This is too much to make a potentially breaking change.

I'm now working on making a tighter use-counter, as spot checks of actual sites make me believe there often isn't an actual contest between the bare declarations and the nested rules (e.g. they use non-overlapping sets of properties).

SelenIT commented 8 months ago

Since the existing behavior follows the pre-existed preprocessors logic and authors start to get used to it, wouldn't it be the easiest solution just to add "nesting depth" as an explicit cascading order criterion between Specificity and Order of Appearance?

LeaVerou commented 8 months ago

Since the existing behavior follows the pre-existed preprocessors logic and authors start to get used to it, wouldn't it be the easiest solution just to add "nesting depth" as an explicit cascading order criterion between Specificity and Order of Appearance?

The Cascade is already insanely complicated, and we've reached the point that most authors don't grok it fully. We should not be increasing its complexity further, and definitely not without good reason!

SelenIT commented 8 months ago

It seems to me that adding one more order criterion to 6-7 existing adds about the same cognitive load as complicating one of the existing criteria with de facto a new implicit reordering rule, but an explicit step in the algorithm would be probably a bit clearer and easier to grok for many people 😳

LeaVerou commented 8 months ago

Possibly true, but there's also the option of not shifting!

Loirooriol commented 7 months ago

Just pointing out that pseudo-elements aren't the only problem, there is also #9806

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<style>
/*<![CDATA[*/
@namespace url("http://example.com/foo");
@namespace svg url("http://www.w3.org/2000/svg");
svg|svg {
  background: red;
  & * { --foo: bar; }
  background: green;
}
/*]]>*/
</style> 
<svg id="svg" width="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"></svg>
</html>

This is currently green in browsers. But if we convert it into

svg|svg {
  background: red;
  & * { --foo: bar; }
  & { background: green; }
}

then it's red, because & is not a type selector so it can only match in the default namespace, and our element is in the svg namespace.

andruud commented 6 months ago

Not sure why this is tagged Agenda+, so this may or may not be relevant, but to follow up from January: improved use-counters are implemented in Chrome 123. Results are not in yet.

css-meeting-bot commented 6 months ago

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 <astearns> ack bramus
<Frances> Tab: Suggest to wait a little longer for use counters
<astearns> ack lea
<Frances> Lea: Use counters trade off in waiting, we might not be able to make the change. CSS nesting is established, how could people use it, have anecdotal data. To continue having shifting data, would like to fix it.
<Frances> Tab: Need to see if it is possible to fix it, will not change unless we can see that it is compatible, in favor.
<Frances> Lea: Don't need to wait for the use counters.
<Frances> Alan: Consensus that it is the right thing if possible.
<lea> s/Don't need to wait for the use counters./Don't need to wait for the use counters to decide whether it's worth doing, only to determine whether it *can* be done./
<Frances> Alan: Would it be possible to make the change, what else do we have to do to see if it will work correctly. Is there a separate issue for SSN?
<lea> q+
<Frances> Tab: A naked ampersand style rule could be possible.
<TabAtkins> The proposal in the issue is to reintroduce @nest without any arguments, which just represents the same elements as the parent. This also avoids problems that would come from a naked `& {...}` rule. Details are in the issue.
<Frances> Matthieu: Would like if we could find the solution in parrallel.
<astearns> ack lea
<Frances> Lea: The author intent is clear, would not like to have another nesting syntax, bad from ui perspective.
<TabAtkins> (We already do this "hack" in MQs/etc, fwiw.)
<Frances> Tab: CSSOM would reflect after the first rule showed up as @nest rules. Could get grouped up and lose their ordering.
<dbaron> s/Could get/Otherwise they would get/
<dbaron> s/reflect/reflect declarations/
<lea> +1 The design component of this does not need use counters and I'm not sure we have consensus on the design we want
<Frances> Alan: It could be useful to continue to work and separate CSSOM into a separate issue.
<Frances> Tab: We could create a separate CSSOM issue.
<Frances> Alan: We can work through it as soon as we get compat data back.
<oriol> Tab's proposal was on another issue, https://github.com/w3c/csswg-drafts/issues/9492#issuecomment-1779739434
mdubet commented 6 months ago

It seems that all the proposals try to work around the fact that we can’t mix declarations and rules because the OM CSSStyleRule/CSSGroupingRule doesn’t allow this : so we either shift the bare declarations up to group them (current behaviour) or rewrite the following bare declarations to look like rules (and because we can’t do that for the first group of declarations inside the rule, we try to find a switch point like first-valid-rule or @nest).

What about introducing a new CSSBlockContent (with contains a list of declarations and rules intermixed) ? It would be the content of style rule, at-media rule, at-container rule, at-scope rule.. - all nested group rules https://drafts.csswg.org/css-nesting/#nested-group-rules

For backward compatibility, CSSStyleRule would derive from it but would also maintain the current CSSStyleDeclaration API behaviour - overwriting similar declarations and ignoring interleaved rules ; but with the new behaviour visible through the CSSBlockContent API to get the list of rules/declarations in the actual cascade respected order.

It introduces some complexity for implementation, but seems doable ?