-
Notifications
You must be signed in to change notification settings - Fork 711
[css-variables-2] Lazy / Late resolving variable mappings #11543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
#2749 is more general, but they're definitely related. This makes me think that perhaps what we need is a I suspect lazy values are easier to design & implement than eager values, but down the line we could also have syntax for the opposite (with a way to provide type). But that's much trickier to design. |
Another day, another bug report rooted in this. User: "Why doesn't my code work to use a larger shadow with a bigger spread?" Their code: wa-avatar {
box-shadow: var(--wa-shadow-l);
--wa-shadow-spread-l: 2.0rem;
} The relevant design tokens: :where(:root),
:host {
--wa-shadow-spread-scale: -0.5;
--wa-shadow-spread-l: calc(var(--wa-shadow-spread-scale) * 0.5rem);
--wa-shadow-l: var(--wa-shadow-offset-x-l) var(--wa-shadow-offset-y-l) var(--wa-shadow-blur-l)
var(--wa-shadow-spread-l) var(--wa-color-shadow);
} Without including every possible mapping in every component's shadow root (so that And even if it were included in every shadow root, this does not help with any other element. Including every mapping on Ideally, it should be possible for people to set either of these tokens at any level of the hierarchy, and have |
The issue with We could define that it's whenever you substitute into a "real" (non-custom) property. But that seems (imo) to unfairly demonize custom properties. They could be used as part of a library, where (to the page author) they're just as meaningfully "baseline" as CSS-defined properties. We could define that it's whenever you substitute into a property with a registered (non-universal) grammar, so custom properties can opt themselves into resolving late() by given themselves a grammar. But not all property grammars are expressible in Finally, I think it's probably reasonable to sometimes want some of the vars in an expression to be resolved immediately, while others are resolved later. On the other hand, we could just say "if you want to define a computation that's resolved at some later time, use custom functions". The evaluation time for custom functions is explicit; it's when they're called. It means there's a bit of a syntax split, but that's something of what we're doing anyway with It's definitely a bit more fiddly to have to define a (global!) function to defer a calculation, but "inline" functions are on the possible roadmap anyway. And they solve the "some evaluated now, some evaluated later", as we've discussed in a custom function issue - reusing the now-dropped I was trying to sketch one out, but my brain is too fried from WG meetings to provide a realistic example right now. Something to work thru, tho. |
Would a syntax like I know that goes against the cycle detection rules, yet I figure it might be worth carving out an edge-case for self-referential loops without any other loop-members or mutations. That'd ideally have |
In general, I think the way we handle |
Background
The fact that
var()
resolves on the element it is specified, while most other things (at least for unregistered custom properties) are passed around as token sequences and are interpreted at the point of usage is one of the things that trips people up a lot and creates a lot of bugs.Example 1: Theme with dark mode
This is a very contrived minimal example to illustrate the problem, so please don't reply "but they can use
light-dark()
for color schemes!".Suppose we have themes represented with
.theme-*
classes, which also specify their tokens on:root
so that if no.theme-*
class is used, the last theme included wins automatically. Each theme also has a.dark
class, for dark mode (which is managed via JS).The author's mental model is that the
--color-border
declaration creates a binding, and if they override--color-bg
anywhere,--color-border
will be updated to match. I.e. that they can use the.dark
class on an element, and everything will just adapt to their dark mode, while in reality, the binding for--color-border
is done on:root, .theme-foo
so it will always be pointing to the light mode values.Fixing it requires definining the mapping in a union of all possible selectors that could override any of its constituent properties:
Demos:
Example 2: Sizing utility classes
Here’s another one:
Can you spot the bug? Unless an explicit
.size-*
class is used on an element,--size-smaller
will be pointing to--size-s
on:root
(or the whatever the closest element with a.size-*
class defines), but the author's mental model was that--size-smaller
should adapt to whatever the current--size-*
variables are on the current element.Both examples are inspired from real recent examples of code I debugged. I cannot count how frequently people seem to hit this issue, and how much trouble they have debugging it.
Strawman
There are use cases where the current behavior works best (it's unclear to me whether they are the majority, but that ship has sailed). Can we have our cake and eat it too, i.e. have ways to get either behavior? I think so. All we need is a way to specify for one or more
var()
references to be late-resolving at the point of usage (essentially like a mini-mixin).var()
would also be propagated as a token stream, just like every other token.*
, since those with a specific syntax are resolved at the point of specification anyway.What makes the design tricky is that there are use cases for defining a bunch of late-resolving declarations, but also use cases for one-offs. Depending on what we want to target, the solution would have a different shape:
1. Target: Multiple declarations, and possibly even entire rules
@late {}
,@lazy {}
,@map {}
etc)var-resolution: late
. Though this kicks the can down the road about when references can be resolved, as they need to wait for selector matching, so it may be harder to implement.2. Target: Whole values of individual declarations
!late
or!lazy
3. Target: Individual values
var-lazy()
,var-late()
or justlazy()
Discussion
We probably don't need all three.
The text was updated successfully, but these errors were encountered: