- From: Lea Verou via GitHub <noreply@w3.org>
- Date: Fri, 27 Mar 2026 20:00:44 +0000
- To: public-css-archive@w3.org
@tabatkins
> If the mixin isn't scoped, then there's no sense in which hygiene exists. Hygienic renames, in any macro system, allow you to create variables _in_ the mixin and refer to them, using the language's existing variable scoping rules, without having to worry about colliding with existing defined names or references.
My assumption was that both mixin arguments, as well as any variables defined outside the `@result` would be transparently rewritten to guaranteed unique names. Is that not possible?
But if names are not rewritten, then they could clash with descendants, which is _extremely_ weird. And if they _are_ rewritten, scoping doesn't matter as a conflict prevention mechanism because they could never clash with variables defined outside the mixin. So I'm now very confused.
I think you may be thinking of languages with lexical scoping? But that’s completely different. On that note, I wanted to address this type of argument that has come up a lot in these discussions:
> There's a lot of possibilities and none of them look particularly close to what arguments do in other programming languages.
First, [internal consistency takes precedence over external consistency](https://www.w3.org/TR/design-principles/#consistency). But also, there are few widely used fully reactive languages, and even fewer (if any) that have the kind of cascade/inheritance AND dynamic scoping that CSS has. So while looking at other languages for prior art is good practice, we should always remember that CSS is quite unique in its design.
> If one wants (a) element-relative values in `@apply` arguments to resolve based on the element that's applying (I think one definitely does)
Yes, we want element-relative values to resolve based on the element that's applying, but the second best thing is them resolving _at all_, using _any_ sensible mechanism. All resolution algorithms have their pros and cons, but having entire rules be ignored, and thus, major capabilities being impossible to define as a mixin, is the worst possible alternative. Especially since there is no escape hatch: all-untyped mixins still have to go through the scoped path *as well*.
Even including the rules with undefined custom properties would have been better — then at least authors could use fallbacks, declarations not using the locals would be fine, and it fits in with the dominant mental model that mixins substitute `@apply` with their `@result`.
> and (b) those resolved values to be usable inside the mixin without confusing restrictions (also something I think one wants),
Having entire rules be dropped *is* a confusing restriction!
----
The message goes on to suggest many _wild_ alternatives to demonstrate that there truly is no other option 🤷♂️:
- No, we don't want to resolve everything at the root
- No, we don't want to "invent a totally novel way of handling arguments"
- No, we don't want to drop typed arguments (though shipping L1 without them would not be the worst thing in the world)
- No, we don't need a new type of variable substitution. If we decide to go down that path we can use the double variable rewrite I outlined above. `var()` resolves to the public name outside the mixin and the rewritten name inside the mixin so I'm not sure where the confusion is. `var(var())` is a little tricky, but not impossible.
- No, we don't need to invent a new variable type
- And last but not least, no, we don't need a time machine 😂
> So. Those are the logical endpoints.
I could be missing something _major_ here, but I see several alternatives to the things above.
First, my assumptions about how hygienic variable rewriting worked have been:
- Variables get hygienically rewritten to guaranteed unique names, per mixin, and there is an internal mapping to the original name. I.e. if `--bar` inside a mixin is rewritten to `--bar-c432fad39`, you cannot reference an outside `--bar` **anywhere** in the same mixin, both declarations AND `var()` references are transparently rewritten throughout. If you want access to an outside `--bar`, rename your local variable! I would find it _extremely_ confusing if a mixin had a `--bar` argument and sometimes that took precedence over an outside `--bar` and other times it didn't. These mappings are scoped to each mixin separately. As a corollary:
- Applying the same mixin multiple times to the same element (e.g. via different rules) would reuse the same mappings. Otherwise simple CSS rewrites such as combining rules or rewriting selectors can end up having unintended side effects.
- Scoping would be shallow per mixin: if the mixin uses `@apply` to invoke another mixin, and the other mixin uses `--bar` as well, that's rewritten to a different `--bar`. If the parent mixin wants to use the same value, they can pass _their_ `--bar` as an argument.
It sounds like this may not be the case for some of them, and I'd love to know more.
But given these assumptions, I see several models that let us have our cake (typed arguments resolve based on the applying element on that element and its descendants) and eat it too (none of the weird things above, no entire rules being dropped because they don't belong to the applying element's subtree):
1. Simply leave these properties undefined on these elements. At least declarations that don't use them still work, and authors can always provide fallbacks.
2. Resolve on the element AND on every other topmost root, then let inheritance handle the rest.
3. Resolve on the element, any top-most root, AND if one of these is an ancestor of the applying element, also resolve separately on every ancestor between the element and that top-most root.
All of these are implementable, I believe. Are they ideal? No, absolutely not. But I think _any_ of them is a lesser evil than the current solution.
And I'm sure there are even better algorithms.
> Either we lean on the variable mechanism that exists today in CSS, which _requires_ scoped rules and _implies_ hygienic rewrites for convenience, or we invent something entirely novel (likely similar to Sass) which doesn't have a concept of the applying element at all and is just rewriting at the stylesheet level.
Rewriting at the stylesheet level with no concept of an applying element is not the actual author need though. I think.
> Those two endpoints are what's _currently_ in the spec, with `@mixin` and `@macro` (with a lot of headroom for `@macro` to develop in the future). There is no middle ground without doing something incoherent (at best, something that's _similar_ to how custom props/var() works but _sometimes different_), as far as I can tell.
But the current mechanism is *also* not how they work. The mental model of "just take the `@result`, replace arguments, and substitute `@apply` fails if they return a scoping root because entire rules get dropped.
Here's an example:
```css
@mixin --foo(--arg <length>) {
@result {
width: var(--arg);
& + p { margin-inline-start: 0 }
}
}
```
Here, `& + p` would get dropped, even though it does not even *use* `--arg`!
> That applies to all five of the suggestions in your OP, unfortunately - they're all trying to hit a middle ground and just doing something _similar_ to custom properties but _sometimes different_, with the differences being somewhat unpredictable and not actually what authors would likely want or expect, as far as I can tell.
I think we're making some dangerous assumptions about what authors want or expect, and we should test those assumptions before trying to resolve tradeoffs based on them.
Literally almost everybody in all polls I posted was **baffled** about the options where `& + p` was dropped (C & D). They found it _more_ surprising than any resolution mechanism whatsoever.
Here are a few quotes:
> I'm not clear where the "nothing" option comes from. Is the idea that since the `+ p` isn't a descendent of the div it can't inherit anything from the mix-in? I don't see why that would make sense unless you're going to make all nesting work like that.
> https://front-end.social/@AmeliaBR/116298405485176051
> Right, but is the possibility of that only there because it's `+` and not `>`? How could it not have a width decoration but the `h2` would?
> https://mastodon.nz/@mez/116297840036968492
> I had the exact same thought process, "what why would it be different in the last case??, hmm okay maybe I'm missing something, but B seems right... Or maybe A 🤔" voted B too.
> https://toot.wales/@Lukew/116297928039987155
> C & D seem yick.
> https://mastodon.bsd.cafe/@gumnos/116298032907042493
> I can't think of a scenario where I would expect C or D
> https://x.com/ranisalt/status/2037311340683944244
> Agreed; I didn't understand why the siblings couldn't work at ALL. I still don't.
> https://bsky.app/profile/ppkoch.bsky.social/post/3mi25thipkc2e
…and many more.
I’m not claiming this was thorough research, it was some quick polls I posted on a Thursday evening to get some quick data. But we should get _better_ data, not just assume!
While none of the alternative resolution mechanisms is perfect, I would be surprised if authors found having entire rules ignored less surprising. Perhaps a simple rename could fix this (`@macro` → `@mixin`, `@mixin` → `@scoped-mixin` or something).
--
GitHub Notification of comment by LeaVerou
Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/13727#issuecomment-4144936033 using your GitHub account
--
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config
Received on Friday, 27 March 2026 20:00:45 UTC