Re: [csswg-drafts] [css-mixins-1] Using a scoping rule breaks author expectations, can we get rid of it? (#13727)

The CSS Working Group just discussed `[css-mixins-1] Using a scoping rule breaks author expectations, can we get rid of it?`, and agreed to the following:

* `RESOLVED: Drop @scope'ing from @mixin. Values that can't resolve because they're outside the subtree are guaranteed-invalid.`
* `SUMMARY: Look into some ideas around merging @mixin and @macro.`

<details><summary>The full IRC log of that discussion</summary>
&lt;TabAtkins> slides at https://webplatform.design/talks/mixins/<br>
&lt;fantasai> Lea: Review of the current state of mixins!<br>
&lt;fantasai> lea: This example uses a sibling selector. But that rule gets ignored because we implicitly wrap everything in @scope.<br>
&lt;fantasai> lea: Is @scope inherently confusing? No. But implicitly using it for mixins is.<br>
&lt;fantasai> lea: You might not want that scoping.<br>
&lt;fantasai> lea: So then we defined @macro which doesn't do the scoping.<br>
&lt;fantasai> lea: But it's very limited, like no arguments.<br>
&lt;fantasai> lea: So I made a poll on social media. I gave ppl a chunk of code with various style rules in a mixin.<br>
&lt;fantasai> lea: What's your expected result?<br>
&lt;fantasai> lea: Here's the results. With 100+ votes.<br>
&lt;fantasai> lea: Caveat -- this mixin has a typed argument. But didn't highlight that point, so can't really distinguish A vs B or C vs D<br>
&lt;fantasai> lea: But if you join A+B  and C+D, you get a very strong signal: 90%+<br>
&lt;fantasai> lea: No expectation that the sibling selector rule would get ignored.<br>
&lt;fantasai> lea: People were *baffled* why it wouldn't work.<br>
&lt;fantasai> lea: Why did we define mixins with @scope? There was a good reason.<br>
&lt;fantasai> ...This example styles the element, ancestor, and sibling.<br>
&lt;fantasai> ... [stuff]<br>
&lt;fantasai> ... Within the non-negotiable constraint that we can't depend on the computed value of elements outside a subtree.<br>
&lt;fantasai> ... Current spec takes nuclear option of using @scope, which ignores outside the scope<br>
&lt;fantasai> ... even if it doesn't have a dependency on this info.<br>
&lt;fantasai> lea: Ignoring the rule is definitey not author intent!<br>
&lt;fantasai> ... the main non-negotiable is that element-relative values must have the same value thorughout the mixin. but if we relax it, and make it *throughout the applying element's subtree*, then we can have more flexibility.<br>
&lt;fantasai> lea: Some options<br>
&lt;fantasai> ... could be guaranteed invalid<br>
&lt;fantasai> ... or resolve per-element and then inherit down<br>
&lt;fantasai> ... or something else.<br>
&lt;fantasai> ... it's not an implementation restriction, it's question of what we want to do<br>
&lt;fantasai> ... 1 is the most straightforward, and authors can provde fallbacks<br>
&lt;fantasai> ... 2-3 might capture intent better, but weirder, might need more author research<br>
&lt;fantasai> ... and then question of typed vs untyped arguments<br>
&lt;astearns> q?<br>
&lt;fantasai> lea: Another option is to rename @macro to @mixin and @mixin to @scoped-mixin or something else that makes the scoping very obvious.<br>
&lt;fantasai> ... Still need arguments for @macro to be usable<br>
&lt;fantasai> ... Maybe use syntactic substitution, like a preprocessor.<br>
&lt;fantasai> ... But then inheritance is a question.<br>
&lt;fantasai> lea: Main questions are<br>
&lt;fantasai> ... 1. Should we do better than dropping the entire rule?<br>
&lt;fantasai> ... 2. Should we tyr to resolve locals, or is guaranteed-invalid ok?<br>
&lt;kizu> q+<br>
&lt;fantasai> ... 3. What do we resolve them to?<br>
&lt;astearns> ack kizu<br>
&lt;fantasai> kizu: I think I agree with everything Lea mentioned.<br>
&lt;fantasai> ... First, yes, we need to do something. Fully dropping rules from author perspective will feel weird.<br>
&lt;fantasai> ... How to resolve locals, good question.<br>
&lt;fantasai> ... If we have a scoped version, should be explicit in the syntax.<br>
&lt;fantasai> TabAtkins: [projects a diagram]<br>
&lt;fantasai> root -> @apply -> [ child --prop "6", child font-size:30px ]<br>
&lt;fantasai> @mixin<br>
&lt;fantasai> TabAtkins: First question is, if we look into mixin, we have one usage of arg1 and one usage of arg2. Feels like they should behave the same.<br>
&lt;fantasai> TabAtkins: These are resolved to some value, and usage inside hte function body is the same.<br>
&lt;fantasai> TabAtkins: We achieve that in the spec by rewriting arg1 and arg2 into magical custom properties on the element.<br>
&lt;fantasai> TabAtkins: that let's us resolve arg1 into a 10em length, becoming 100px<br>
&lt;fantasai> TabAtkins: and then those inherit normally.<br>
&lt;fantasai> TabAtkins: Children and ::before will all have consistency<br>
&lt;fantasai> TabAtkins: That's implemented via custom properties and inheritane.<br>
&lt;fantasai> TabAtkins: Complexity is if you bring in more elements and then extend mixin to also style the sibling and the nibling.<br>
&lt;fantasai> TabAtkins: What happens here? Assumptions that all usages inside our mixin apply here<br>
&lt;fantasai> TabAtkins: but there's no way to reference the size of an 'em' or the value of a custom property on siblings or cousins.<br>
&lt;lea> q?<br>
&lt;lea> q+<br>
&lt;fantasai> TabAtkins: if we want arguments to be able to resolve, for basic circularity reasons, they have to get scoped<br>
&lt;fantasai> TabAtkins: Lea's proposal is that arg1 and arg2 don't always resolve to the same value.<br>
&lt;fantasai> TabAtkins: if they apply to something ouside the tree, they 'll resolve to something else. Possibly invalid.<br>
&lt;fantasai> TabAtkins: I don't like that happening. Fact that a var resolves to a predictable stable value, but used other places depending on where in the tree, is an odd behavior.<br>
&lt;fantasai> TabAtkins: It's also weird if you're not using vars at all you can't ge tit<br>
&lt;fantasai> TabAtkins: because rules are scoped<br>
&lt;fantasai> TabAtkins: I don't want to have it where you can use rules outside the scope as long as they're not using a variable reference<br>
&lt;fantasai> TabAtkins: seems like a complicated boundary for authors to learn about<br>
&lt;fantasai> TabAtkins: scoping is not obvious. But you can learn it.<br>
&lt;fantasai> TabAtkins: Now we also have @macro so you can use that.<br>
&lt;fantasai> TabAtkins: Example<br>
&lt;fantasai> @macro --foo(--arg) {<br>
&lt;fantasai>    &amp;, &amp; > *, &amp; + * { width: var(--arg); }<br>
&lt;fantasai> }<br>
&lt;fantasai> .foo { @apply --foo(10em); }<br>
&lt;fantasai> This would desugar to a literal substitution of 10em. It would resolve locally on each element it resolves on.<br>
&lt;fantasai> TabAtkins: every usage is the same, they all resolve on the local element.<br>
&lt;kizu> q+<br>
&lt;florian> q+<br>
&lt;fantasai> TabAtkins: Pass in a variable, same thing. It resolves locally on each element.<br>
&lt;miriam> q+<br>
&lt;fantasai> TabAtkins: My preference would be to have @macro do the literal writing. And predictable. Can achieve some of the use cases by haivng intermediary variable.<br>
&lt;fantasai> --test: --as-length(var(--prop));<br>
&lt;fantasai> &amp;, &amp;>*,&amp;+* { width: var(--test); }<br>
&lt;fantasai> TabAtkins: If others lean towards Lea's position, then the only case that I think makes sense is making things invalid.<br>
&lt;fantasai> TabAtkins: If we need to have unscoped rules in the mixin, then that's the only thing that seems ok.<br>
&lt;astearns> ack lea<br>
&lt;fantasai> lea: I think what you presented is good for @macro anyway. It does need more research if syntactic rewriting covers all use cases. What happens with transitions/animations if no real property, for example?<br>
&lt;fantasai> lea: But the reason I hear for @scope inside @mixin is because we don't like what authors are doing.<br>
&lt;fantasai> lea: It's not actually necessary though.<br>
&lt;fantasai> lea: And no way for them to escape the @scope rule.<br>
&lt;fantasai> lea: They can always include one if they want it, if we don't force it.<br>
&lt;fantasai> lea: Nothing would change inside the scope either way, only unscoped rules behavior would change.<br>
&lt;fantasai> lea: Mental model of mixin is to avoid name clashes, and defined on applying element, and what happens with regular properties happens.<br>
&lt;fantasai> lea: Yes, authors can learn and live with it as they have for many other things that are confusing in CSS.<br>
&lt;fantasai> ... just because authors can use learn it doens't mean we should burden them with learning<br>
&lt;fantasai> lea: Wrt macro arguments, what's the point of typing?<br>
&lt;astearns> ack fantasai<br>
&lt;TabAtkins> scribe+<br>
&lt;TabAtkins> fantasai: my impression si that what makes the most sense is to not scope the contents of the Mixin block<br>
&lt;TabAtkins> fantasai: that affects specificity, it cuts out the out of scope rules. several effects<br>
&lt;TabAtkins> fantasai: I agree with Tab where if there's no reasoanble way to resolve the variables, it's the GIV<br>
&lt;TabAtkins> fantasai: so I think you'd not scope them...<br>
&lt;florian> q?<br>
&lt;fantasai> fantasai: and use the guaranteed-invalid value if it's dereferenced on an element that cna't resove it<br>
&lt;astearns> ack kizu<br>
&lt;fantasai> kizu: I'm not sure we need typed arguments for macros.<br>
&lt;fantasai> kizu: Maybe there's a value for mixins, but it can create confusion.<br>
&lt;lea> I would def support L1 to only have untyped arguments<br>
&lt;fantasai> kizu: I think from author perspective, invalid at computed value time is reasonable, and learnable<br>
&lt;fantasai> kizu: If we could have an ident function and you wanted to resolve it inside the mixin and not where you code, we could maybe do that<br>
&lt;fantasai> kizu: [gives an example]<br>
&lt;fantasai> kizu: If we do that, what's the difference between macros and non-macros.<br>
&lt;astearns> ack florian<br>
&lt;fantasai> florian: Thing Tab presented as macros will be intuitive to people, acts like a preprocessor.<br>
&lt;fantasai> florian: The behavior of mixins is arguably more useful. But not obvious from its name that it's what it does.<br>
&lt;fantasai> florian: So maybe naming can help?<br>
&lt;fantasai> florian: They're basically resolved macros. Resolve it on an element, and then apply from there.<br>
&lt;fantasai> florian: Then whether it's the scoping that makes it clear, in order to resolve you need to scope to a tree, or the other option that you like less, yes you can have rules outside but values there will be invalid<br>
&lt;fantasai> florian: that's also understandable. If you resolve to a subtree, and try to use outside subtree, of course you can't get it.<br>
&lt;fantasai> florian: I'm probably leaning towards param values are invalid, but the rules still are there.<br>
&lt;lea> q+<br>
&lt;fantasai> florian: but finding a helpful name to hint people why they are different ...<br>
&lt;astearns> ack miriam<br>
&lt;fantasai> miriam: I'm agreeing with most others, esp florian<br>
&lt;fantasai> miriam: It seems the more powerful we make macros, they do just about everything I want from a mixin<br>
&lt;fantasai> miriam: Idk that resolving on the called element is that important that it determines what a mixin can do<br>
&lt;fantasai> miriam: So I would support basic and flexible mixin, and then have a more powerful variant of mixins if we need it<br>
&lt;astearns> ack lea<br>
&lt;florian> q+<br>
&lt;fantasai> lea: I do think one of the two should be named @mixin.<br>
&lt;fantasai> lea: Like, people are still asking for parent selector , because we never made one even though we have the functionality.<br>
&lt;fantasai> lea: I don't care much about the scoped one, probaby wouldn't use it much<br>
&lt;astearns> ack florian<br>
&lt;fantasai> florian: What if, given the difference is about resolving, we make the resolving explicit?<br>
&lt;fantasai> florian: Like in your mixin macro, you say what element it resolves on, it resolves it<br>
&lt;fantasai> florian: you could resolve on parent or whatever<br>
&lt;fantasai> florian: and if you don't resolve you get macro behavior<br>
&lt;miriam> (resolve on named container?)<br>
&lt;fantasai> florian: and because you explicitly do this, it's less magical<br>
&lt;fantasai> +1 to the idea but needs more concreteness :)<br>
&lt;fantasai> florian: An maybe have some shortcuts for the common cases<br>
&lt;fantasai> miriam: A named container would be guaranteed parent of everything<br>
&lt;ntim> what issue are we talking about<br>
&lt;fantasai> TabAtkins: proposal that if you reference mixin argument or local from an element that can't see the applying element, that resolves to guaranteed-invalid value<br>
&lt;fantasai> TabAtkins: so not scoped like @scope<br>
&lt;lea> +1<br>
&lt;fantasai> PROPOSED: Drop @scope'ing from @mixin. Values that can't resolve because they're outside the subtree are guaranteed-invalid.<br>
&lt;ntim> q+<br>
&lt;fantasai> RESOLVED: Drop @scope'ing from @mixin. Values that can't resolve because they're outside the subtree are guaranteed-invalid.<br>
&lt;lea> PROPOSED: `@macro` (or whatever it’s called) should get (untyped) arguments<br>
&lt;fantasai> fantasai: Seems like there was an interest in possibly merging and macros, which has a lot of options to think through.<br>
&lt;fantasai> SUMMARY: Look into some ideas around merging @mixin and @macro.<br>
</details>


-- 
GitHub Notification of comment by css-meeting-bot
Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/13727#issuecomment-4173187508 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Wednesday, 1 April 2026 21:53:29 UTC