-
Notifications
You must be signed in to change notification settings - Fork 759
Description
As currently defined, mixin parameters are not evaluated in the context of the element they appear to apply to. Example:
<div class=foo>
<div class=bar style="--theme:red; font-size:8px"></div>
</div>@mixin --colorize-tree(--color <color>) {
&, & * {
border-color: oklch(from env(--color) calc(l/2) c h);
accent-color: oklch(from env(--color) calc(l/2) c h);
}
}
.foo {
--theme: green;
@apply --colorize-tree(var(--theme));
}The above @apply statement would at first appear to colorize the tree green. In reality the var(--theme) gets interpreted against each element it applies to; <div class=bar> gets colorized as red.
The same is true for element-dependent values in general, e.g. em units:
@mixin --pad-tree(--w <length>) {
&, & * {
padding: env(--w);
}
}
.foo {
font-size: 20px;
@apply --pad-tree(1em);
}The above looks like it applies a padding of 1em = 20px to the whole subtree, when in reality it's using the font-size of the elements that are ultimately matched; <div class=bar> gets a padding of 8px. See also this relevant example in the specification.
Short of disallowing computationally dependent values, there is not much we can about this in the current model; env() traverses lexical scopes looking for a matching name, without any knowledge of elements at all.
In the var()-based model for mixin parameters, parameters and locals are based on the same dynamic scoping model proposed (or at least spearheaded) by @LeaVerou (#10954). This means we could potentially perform a "hygienic rewrite" (so called by @tabatkins) of the parameters; the --pad-tree example becomes effectively:
@function --f(--w <length>) {
result: var(--w);
}
.foo {
font-size: 20px;
/* Secret, unobservable custom prop from the @apply call: */
--pad-tree-param-1: 1em; /* Registered type: <length> */
&, & * {
padding: --f(var(--pad-tree-param-1));
}
}Here, 1em is interpreted at the @apply-site, and made available in computed-value form to the subtree.
This works only when the mixed-in rules all select (inclusive) descendants of the @apply element, hence the mentions in #12927 about mixins optionally being scoped. Applying a scoped mixin would produce behavior similar to wrapping an @apply call in a @scope rule:
@mixin --push-sibling() {
& + * {
margin-left: 200px;
}
}
div {
@apply --push-sibling();
}would expand approximately to:
div {
@scope (&) {
& + * { /* Not in scope */
margin-left: 200px;
}
}
}This scoping would give us a guarantee that any information "captured" on the @apply element due to hygienic rewriting will be available to anything that gets mixed in. The obvious drawback is that elements outside the "apply root" can no longer be matched.
There are several open questions about scoped mixins:
- Are they scoped by default, and then you opt out? (Or vice versa?)
- How do you opt-in/out?
@mixin unscoped --m() {}? - Or do you opt-in/out on the call site?
@apply unscoped --m().
cc @mirisuzanne