- From: Lea Verou via GitHub <sysbot+gh@w3.org>
- Date: Mon, 20 Jan 2025 18:39:57 +0000
- To: public-css-archive@w3.org
LeaVerou has just created a new issue for https://github.com/w3c/csswg-drafts: == [css-variables-2] Lazy / Late resolving variable mappings == ## 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). ```css :root, .theme-foo { --color-blue-95: #ebf4ff; /* ... */ --color-blue-05: #00112f; --color-blue: var(--color-blue-50); /* "Semantic" color tokens */ --color-text: var(--color-blue-05); --color-bg: var(--color-blue-95); /* "Semantic" color token mappings */ --color-border: color-mix(in oklch, var(--color-bg) 70%, var(--color-blue)); } @scope (.dark) { &, .theme-foo { --color-text: var(--color-blue-95); --color-bg: var(--color-blue-10); } } ``` 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: ```css :root, .theme-foo, .dark { /* "Semantic" color token mappings */ --color-border: color-mix(in oklch, var(--color-bg) 70%, var(--color-blue)); } ``` Demos: - [Testcase](https://codepen.io/leaverou/pen/wBwEJvz/ace2df99fcfcf9f98e42c93ec5f88a71) - [Testcase (fixed)](https://codepen.io/leaverou/pen/zxOJNbj/abf770cfbb661f566659702759f380ce?editors=1100) ### Example 2: Sizing utility classes Here’s another one: ```css :root { /* Base styles */ --size-xs: var(--font-size-xs); --size-s: var(--font-size-s); --size-m: var(--font-size-m); --size-l: var(--font-size-l); font-size: var(--size); } .size-s { --size: var(--size-s); --size-smaller: var(--size-xs); } :root, /* Medium size is the default */ .size-m { --size: var(--size-m); --size-smaller: var(--size-s); } .size-l { --size: var(--size-l); --size-smaller: var(--size-m); } .callout { /* Callouts should be generally larger */ --size-xs: var(--font-size-s); --size-s: var(--font-size-m); --size-m: var(--font-size-l); --size-l: var(--font-size-xl); } ``` 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). - Late-resolving would mean that rather than the current behavior, `var()` would _also_ be propagated as a token stream, just like every other token. - This might need to only work for unregistered custom properties, and those with a syntax of `*`, 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 1. an @-rule containing the declarations (e.g. `@late {}`, `@lazy {}`, `@map {}` etc) 2. An empty @-rule, whose scope is its lexical block (and the whole stylesheet if specified outside any blocks) 3. An inheritable property, e.g. `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 - A !-annotation, e.g. `!late` or `!lazy` ### 3. Target: Individual values - A separate function, e.g. `var-lazy()`, `var-late()` or just `lazy()` ### Discussion We probably don't need all three. - Any of them can emulate all others (emulating 3 would involve extra declarations), just with more friction. If the function name can be shorter, that reduces the friction. - I think a graceful fallback to the current behavior could really help adoption. 1.2 and 1.3 are the only ones that doesn't break in unsupporting browsers. - The vast majority of use cases I have encountered are about many declarations (often dozens), so some version of 1 seems most useful. - I suspect 3 is easiest to implement. Then perhaps some variant of 1 can be implemented as sugar over it. Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/11543 using your GitHub account -- Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config
Received on Monday, 20 January 2025 18:39:58 UTC