-
Notifications
You must be signed in to change notification settings - Fork 688
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
Proposal: CSS Variable Groups (as a solution to several design systems pain points) #9992
Comments
This looks so nice! And yes, I need that not just yesterday, but 5 years ago. A question I have is that it's not clear how to get the root property name from a specific one, i.e. from I'm not sure if there could be much of a use case... Maybe if I want to use another variant with a different specifier, i.e. using |
Will we have an implicit Group creation if we define variables that share the same prefixes? If for example I do the following: :root {
--color-red: red;
--color-blue: blue;
} Does it mean that, automatically, we have the group variable |
Could be this proposal unified with @Afif13 I think property groups are here to handle custom properties managing complexity. Groups do not have any value for its own sake. Otherwise it would be mentioned in the proposal. |
They do based on the whole proposal as we can do something like :root {
--color-green: {
100: oklch(95% 13% 135);
900: oklch(25% 20% 135);
}
}
my-component {
--color-primary: var(--color-green);
} So if I define |
That's a good point. I don't think however, that it should be propagated to auto-magically created groups. Could be a source of funny bugs. |
I like this and I wonder if it can also solve length values? In design systems it is really common to also have a bunch of length values for margin, padding, font-sizes,... Design tools only offer the ability to export everything as pixels or as rem. So we end up writing this mess : :root {
--space-1--px: 1px;
--space-1--rem: 0.0625rem;
--space-2--px: 2px;
--space-2--rem: 0.125rem;
--space-4--px: 4px;
--space-4--rem: 0.25rem;
--space-6--px: 6px;
--space-6--rem: 0.375rem;
--space-8--px: 8px;
--space-8--rem: 0.5rem;
--space-15--px: 15px;
--space-15--rem: 0.9375rem;
--space-16--px: 16px;
--space-16--rem: 1rem;
--space-18--px: 18px;
--space-18--rem: 1.125rem;
/* goes al the way to 96 ... */
} Maybe there is something to custom units which are also a kind of custom prop? |
Interesting stuff!
Did you consider following compound nesting syntax? --color-green: {
&-100: oklch(95% 13% 135);
&-200: oklch(95% 15% 135);
/* ... */
&-900: oklch(25% 20% 135);
} Pros: no new pattern of an implicit separator, eliminates initial-digit question. Con: more verbose, could imply that With that change, the essence of this proposal would be to extend nest syntax beyond selectors to custom properties. That extension feels natural to me, and the doors it opens might be valuable if the future is heavier in custom property use.
But what are the contexts which would support this magic key but not groups? I've built from designs that used "default" as a color level, and have been glad to have that word available. This feels to me more radical (new "magic key" pattern) and restrictive (can't use that word in a single-word nests) than it's worth to make this --color-green: {
base: oklch(65% 50% 135);
} create Removing the magic eliminates the question "how do we override just the default value?": --color-green: {
base: oklch(65% 50% 135); /* styles --color-green-base */
}
--color-green-base: oklch(65% 50% 130); would work to make core green a little yellower, with no special case needed. Share more about the motivation?
In what way is the result different from removing the first three lines? my-component {
--color-primary: var(--color-green);
background: var(--color-primary-200);
}
/* design-system.css */
:root {
--color-green-100: oklch(95% 13% 135);
--color-green-200: oklch(95% 15% 135);
/* ... */
--color-green-900: oklch(25% 20% 135);
} |
I really like this, and the authors wanted to have an ability to have arrays, lists, and maps in CSS for years! This proposal sounds good enough to cover most of their use cases on the first glance.
If I understood the proposal correctly, the part that covers this is the functional syntax:
With it, we could potentially do .note {
color: --color-primary(dark, 75);
} And get the value of I 100% sure we need some way to dynamically extract values, and, especially, use dynamic variables to do so, like What I'm not sure about is the syntax: I am not sure that promoting the variable groups to functions/mixins is a good idea, as it makes it easy to clash with the regular functions, and makes the double-dashed functional syntax tightly coupled with the variables themselves. If I proposed something, I'd try something like a Some other quick unsorted notes after reading the proposal:
I'll need to spend more time thinking about this proposal, but I just want to reiterate how excited I am about it. |
I'm really digging this idea! I had a few questions, and maybe some possible gotchas—
Thanks! 🙂 |
I would like to contribute a critical perspective to the whole design-system aliasing topic in css vars. Sorry if this is a bit off topic. In my experience this causes more problems than it solves, and may encourage dark patterns, at least at the moment. For example:
I know each design system is different, and there maybe usecases, where it makes sense to do more in the browser. But i don't think the general advice to do everything in the browser now, should come without a disclaimer about the disadvantages and that server side css tooling will never go away completely, at least for large projects. In this regard, this proposal seems like it will also help reducing the file size aliasing can contribute to. (But i would still advice anyone not to do it completely in the browser, if not neccessary) I think a good combination of both is they key here, depending on the usecase. So the proposals that focus on extending css with design system focussed capabilities, are very welcome (like this and css functions)! Don't get me wrong. Just trying to support with input, on what can go wrong. |
My initial response to the green example of group assignment in isolation Of course there is always room for naming convention to suggest the “groupness” of a variable. For example, capitalising group names I very much like how the standalone |
So, you want custom longhand properties with systematic names based on their shorthand. I think you should make clearer that whatever syntax this ends up using could likely also be backported to normal properties. Could it be possible to drop :root {
--color-green:
oklch(65% 50% 135deg) {
&-100: oklch(95% 13% 135deg);
/* … */
&-900: oklch(25% 20% 135deg);
};
font: serif {
&-size: 2em;
line-height: 3em;
};
}
Still, it makes me wonder whether this could be solved even better in a way specific to colors, because this reminds me a bit of CNS (with custom base values). Alas, everything I can think up allows arbitrary variants, e.g. 0% through 100% for some parameter, which goes against the philosophy of (atomic) design systems which try to increase maintainability by limiting the implementation choices for authors. |
cc #9206 |
Hi everyone! Wow, lots of comments. Some overall comments:
--color-green: oklch(65% 50% 135) {
100: oklch(95% 13% 135);
200: oklch(95% 15% 135);
/* ... */
900: oklch(25% 20% 135);
}; However, one of the concessions we had to make to make
--color-green: oklch(65% 50% 135);
--color-green-*: {
100: oklch(95% 13% 135);
200: oklch(95% 15% 135);
/* ... */
900: oklch(25% 20% 135);
}; Similarly, some folks mentioned they’d like a more explicit syntax that makes it clearer that multiple properties are being set, e.g. by setting
Specific replies: I like this and I wonder if it can also solve length values?
Absolutely! I used colors as an example just because it's the most complex part of most design systems, but everything is meant to apply to entire design systems. --space: {
px: {
default: calc(1px * arg);
};
rem: {
default: calc(arg / 16 * 1rem);
}
} If anything, this use case highlights how important it is to be able to get programmatically defined tokens! Dynamic utility classes
This is not just Tailwind, the FontAwesome folks were recently telling me about this exact same problem. They would love to be able to have But even beyond these types of use cases, there are tons of use cases where you’re basically using class names with a common prefix as essentially key-value pairs, and need the equivalent of attribute selectors and I think this could actually be a separate feature to let you get the part after the prefix and target class names by prefix, and then together with this proposal you can implement the mapping. Something like this: [class~^="bg-"] {
background: get(--color, class-suffix(bg-*));
} WRT base values
The main motivation is twofold:
I’d love to hear more about the cases where you want to have a value called "base" or "default", but it's not actually the same value as you want the property to return when no suffix is used. One thing I haven't seen mentioned yet (but maybe everyone just is aware/understands and sees no issue) is that the proposed syntax is already valid:
...which is good in some ways (no parsing changes) but also means this would be adding new behavior to any projects already storing JSON-y type stuff in a custom property, which as I understand was an intentional design goal of custom properties from the beginning, to allow storing things you might pull out using JS. This came up in the nesting discussions, and we did the research then, it turns out there were exceedingly few websites actually doing this.
Then
Yeah, that’s one of my concerns as well.
I love this idea!
"Fallback" implies that this is a value that is used when something goes wrong, which is not how I see it at all. While it is also used as a fallback that’s not really its primary purpose. I see of it a bit like
Use cases?
Yes.
As I replied to @kizu above, I think a functional syntax that uses a separate function is the way to go when arbitrary keys are desired. However, it should not be the only way because these tokens are used all over the place, so even a little added verbosity adds a lot of friction. Also, there are multiple components to this problem, and referencing arbitrary variants is not the top pain point. naming collisionsIf I am in this situation: :root {
--color-primary-100: black;
--color-green: {
100: oklch(95% 13% 135);
200: oklch(95% 15% 135);
/* ... */
900: oklch(25% 20% 135);
};
}
some-component {
--color-primary: var(--color-green);
} I'm not sure how clear it is that by—essentially—spreading those grouped values into the new Are there any use cases where this is a bug, not a feature?
I think that’s a separate issue, but also I disagree that semantic tokens are the only true way and the rest of the color palette should be hidden. That just ends up adding bloat to the design system as an attempt to cater to reality. But as you say, this is off-topic.
Yes! That is definitely a very explicit goal of this proposal.
Yeah, I was thinking of this too; there are certainly use cases where you want the definition to be continuous for author convenience, but the tokens exposed to still be discrete (with a way to easily change the range and granularity). Perhaps
With the current proposal you cannot (just like in JS you can't get from Will we have an implicit Group creation if we define variables that share the same prefixes?
This is covered in the proposal. Not by default, you’d need to set
The issue is that |
Re:WRT base values (the comment you quoted was me not Adam. As a LESS proponent turned Tailwind proponent myself, I'll take the conflation 😄)
Not sure that I'm understanding. I think you're saying to ensure that given
I wasn't clear, that's not what I had in mind. But here's a contrived example for it: a palette with ten shades of green, where shade 3 is the starting point from the other nine shades are calculated, and where shade 5 is the "primary" shade (think the 400 font-weight). In the group definition you'd have top level names 1, 2, base, 4, primary, 6, 7, 8, 9, 10, and the variables would be green-1, green-2, green, green-4, green-primary, green-6, etc. Usable if you know the rule, but arguably not an accurate reflection of the design. Especially in colors where a designer might not give special distinct meaning to "base" vs "default", or might have their own idiosyncratic distinctions, and in CSS where we don't already have magic keys, I'm not sure the language should read meaning into any key name. That's off the top of my head. What I had in mind: Say the magic name is
I've hit all of those when using Tailwind's magic tldr I want to have the choice to have a key "base" or "default", and for the language to not swallow that key (and I don't expect the language to let me leave off a key)
Then does magic --a: {
base: {
b: 1;
}
b: {
base: 2;
}
} |
This is really very nice. I don't tend to have the colour mapping issue as described in the proposal. I do have it in other ways. --color-link: {
base: darkblue;
--hover: dodgerblue;
--visited: purple;
--active: crimson;
}
--typography-weight: {
/* within the group refer to the extension name only? */
base: var(--semibold);
--thin: 100;
--extralight: 200;
...
} The scary part (okay, not very scary) is that changes to the extension names/keys could break further down the chain. But certainly more manageable than mapping everything by hand! I wonder if it would be possible to preconfigure the extensions in an at-rule property. @property --color-blue: {
syntax: "<color>";
extensions: 100 / 900;
}
@property --stroke-width: {
...
extensions: thin | medium | thick;
} In usage: --color-blue: {
--10: lightblue; /* Would show a property error */
--200: blue;
--300: darkblue;
}
|
I think getting out of sync is not a problem. That's a user problem and not this problem. For me, that's perfectly fine. I also agree with Crissov on the syntax. It's always better to use some sort of syntax than having a reserved word. I also believe that if this is accepted, it should be seen as syntax sugar the same way that CSS nesting could have been. |
Great ideas here! Just reposting here what I already posted on X too. Some people use BEM like notation for color variants, so e.g. :root {
--color-primary*: {
base: #00f;
--100: #66f;
}
} The asterisk/wildcard at the end of the variable name could tell the parser: this is a variable group. To reuse a variable group, one can use: .card {
--card-color-primary*: var(--color-primary*);
} This makes it very clear that we're reusing the entire variable group, not just the base value. Now if you were to leave out the asterisks, you would just use the base value only: .card {
--card-color-primary: var(--color-primary); /* no asterisks at the end */
} |
Seems like the syntax as proposed would potentially be a breaking change? The following CSS already parses as valid today: .foo {
--color-green: {
100: oklch(95% 13% 135);
200: oklch(95% 15% 135);
};
} Someone might use a syntax like this to share data between CSS and JS. Not sure how common that is but changing the custom property syntax to reserve Maybe a new at-rule for defining these could work? Or add something to |
I think that in JS, if we read the computed style for a grouped property value, it should return the full group rather than the It could be worth introducing new JS APIs to access and read/write grouped values, like this: getComputedStyle(elem).getGroupedPropertyValue('--color', 'primary', '100') I believe this addresses @devongovett's point as well. As he mentioned, that syntax was valid and would still be considered valid today. As long as JS continues to yield the same results it currently does and gets the full group, I don’t see this causing any breaking changes. I can’t speak for every library, though I myself do maintain a library for sharing data between CSS and JS in this fashion, and I don't think this would cause any breaking changes for my library, so long as JS still retrieves the entire property value, not just the Even without a new JS method for retrieving nested properties, a JS helper function could still be written to perform such a task as needed, so it's not really a blocker for this feature. A case could be made for requiring the use of the Nearly all CSS custom property values—including JS—parse as valid, though I don't think this would cause any tangible breaking changes, as this would just parse as valid in contexts where it isn't currently, e.g., used as the value for a color property. Passing it into another CSS variable or consuming it in JS would still retain the full grouped value as it does today. That said, it does feel risky to have one CSS custom property essentially generate any number of others. This could be confusing for both new and seasoned developers trying to understand where a variable is coming from, e.g., searching their codebase for a static variable name I would personally prefer a dedicated syntax for referencing nested properties, such as a dot syntax, which would feel familiar and support both strings and numbers in CSS. So, this would be valid syntax: I think the value of use cases for |
It has side effects though: .foo {
--foo-bar: red;
--foo: {
bar: green;
};
} If we make |
@devongovett Yes, I too noted that concern earlier in this thread (under "naming collisions"). I do think that side effect is a rather significant one. Even for future development, it's not super clear that one variable group may overwrite any number of other variables. I see a couple of possible solutions to this problem:
I personally find
I elaborate on this in my previous comment. |
After thinking a bit about it, I don't like the idea of magically creating other variables from a map, and will be ok with having just a The thing we'd need then is to have something like |
(Sorry, I've only skimmed the rest of this thread; it got real long real fast.) Overall, several good ideas in here. I think it could do with a more holistic look at the use-cases, tho - as written, it gradually grows more complex and reinvents itself in several forms. I think deciding on what use-cases to address, and solving them directly, will result in a better proposal than what's currently written. Being able to define parametrized groups of variables and pass them around as a group makes sense. There's several distinct ideas here:
I think being able to refer to variables by prefix, and possibly rename them by prefix, makes sense. The syntax suggestions are fairly lightweight, and it doesn't meaningfully overrun any other functionality in the language. We will need some syntax to differentiate it from existing variables, tho. The existing syntax space is wide-open, intentionally, so we can't just infer grouping from the property name, or from the value being a I would love to see specific examples of people using design systems and running into these problems, however, to make sure we are indeed solving their problem. This is designing syntax/tooling, and the use-cases are necessarily more abstract, but we still need to be sure we're actually helping people. As we get further down the list, tho, we're basically just doing functions. (Or array/dict lookups, which can be seen as particularly simple functions; or vice versa, functions are particularly complex array lookups.) We already have a proposal for doing CSS functions, and I don't think we should do functions twice, with different syntaxes, unless there's a really good reason. Near the end, you argue against the usage of custom functions for handling several of these. I think we should look at this possibility more carefully, as functions do substantially overlap in use-cases here. You mention having to write a new function as being heavyweight; I acknowledge it's certainly heavier than writing some variant of (But still, if we did introduce something like the
I think this is a plus, fwiw. Making it clear what's variable and what's not is a good thing; people shouldn't be assuming that every dash-separated suffix of a variable name can be removed to form a more generic variable. A --color() could exist, tho, once we actually introduce the conditional functions we've bandied about. (And we should do so, as part of the custom functions proposal, imo.) That way you could write @function --color(--type, --tint: 40) {
result: cond(
(var(--type) == primary) --color-primary(var(--tint)),
...
);
} Or an equivalent syntax with at-rules.
Sure there is. You can't pass functions around the cascade, but functions can depend on custom properties, so like: @function --color-green(--tint: 400)
using (--color-green-100: oklch(...), --color-green-900: oklch(...)) {
result: color-mix(in oklch, var(--color-green-100) calc((100 - var(--tint) / 10) * 1%), var(--color-green-900))
} This calculates the full green spectrum off of the start/end greens, and allows them to be overridden in any subtree.
Assuming the conditional functions end up existing, then you could always test for the precise values you want to define for, and let any non-matching values resolve to something invalid. Or you can round to the nearest value you want to handle. Several possible options, and it gets larger as we expand CSS's value space. I think the biggest issue with using functions is that it wouldn't mesh with your "automatic grouping by prefix, so others can extend it" goal. The Like, if we think this sort of "define groups of functions with a common prefix, and dynamically dispatch to them" will be common, we could just make dynamic dispatch work. Like: @function --color(--type, --tint: 40) {
result: function-call(--color / var(--type), var(--tint));
} The first argument to |
Thanks for the careful look @tabatkins, I’ve been looking forward to your reply!
That's a nice overview. I think the decomposed proposal may mesh better with that line of thinking.
Yeah, I think that makes a lot of sense, and removes a lot of the magic. A few thoughts as I read your reply:
Did you see the entire table of popular design systems that I included in my proposal?
It's not just about verbosity. I have no idea how you'd offer something where the design system specifies the ends and the midpoint, and the other tints are automatically computed BUT you can also add hand-tweaked variants to influence how the interpolation works. Though perhaps that is better addressed in
Groups are analogous to objects/dicts, not functions. Dynamic groups do encroach into function territory, but they are more analogous to JS proxies than functions. You could argue that everything can be implemented via functions (and Lisp even argues that everything can be implemented via lists) but typically languages offer data structures as well, because implementing data structures as functions is painful AF.
It's not primarily about it being heavyweight (though as an author, looking at your code examples of how it could be done has me going NOPE NOPE NOPE I’d take the repetition over this 😅). That's the least of it. I think possibly the biggest issue is around encapsulation. Whether the palette is defined as continuous or not, or somewhere in between should be an implementation detail, not drive how you refer to design tokens. The final user of the design system should not have to care about whether Also, unless we introduce some kind of syntactic convenience, having to use functional syntax is an all-or-nothing preposition which does not play nicely with composition across a distributed ecosystem, while my proposal had paving the cowpaths and requiring as little shared contract between context and components as possible as explicit goals. But even if the component did use functions for its design system, how would you pass these to a component? None of the proposals around functions includes a concept like a function reference. |
I suspect we don't want to allow that, just because that means we have to be a lot more literal with the nested bits. Like, we'd have to allow
Yes, but that's not what I was asking for. I'd want to see worked-out examples of how such design systems would use this, and how it would make their usage of their own design systems easier and more reliable. Ideally every reader of the proposal doesn't have to do the working out themselves. ^_^
Can you give an example?
My argument isn't that we should treat data structures and functions as equivalent. It's that your proposal spans the full gamut from "obvious data structure" (variable groups) to "obvious function" (variables with continuous variation), and we should be careful about how we handle this. CSS is already going to grow functions in some way. Do we want two distinct function syntaxes - one defining globally using at-rules, and one defining locally using properties? If so, how much can they differ in power/expressiveness? How much flexibility do we even have in declaration syntax? How much will this cost us in terms of future syntax flexibility? Currently, I'm very hesitant in trying to define a separate, second function syntax, and extra hesitant about doing so within the bounds of declaration syntax. I would need an extremely compelling argument that it's both necessary, and the only way to do so, before I'd be willing to go for it. (I'm super extra hesitant about doing any of this before we have even actually defined the first type of functions.) Because we still have to answer the question: how much must we do? Can we do a fairly small feature (variable groups) that solves 90% of the problem, and just skip the last 10%, thus avoiding having to define a massive new feature (declaration-syntax cascading functions masquerading as custom properties)? If that 10% does need to be solved, are there other ways to do it that aren't as heavyweight of a feature? Maybe the split is actually that the small, easy feature can only handle 50%. Maybe the remaining issues, while unsolved, are fine to just leave as something that's a little awkward to write. In the absence of answers to those questions (reasonable, because this is a very early exploration into the space!), I'm trying to push on the boundaries a bit. How much can we solve using existing (or planned) features instead? If we do have to introduce new features, what other possibilities exist that might be lighterweight in syntax and/or functionality?
Generally speaking, I disagree with this! What you expose and how you expose it are important considerations in API design. If, in JS, I hand my user an object and they're expected to access properties on it, that implies that there's a finite, predetermined number of values I can access. If I hand them a function that takes a continuously-varying argument, that implies that there's a continuously-varying output value. If I swap either of these, I'm somewhat violating expectations, and better have a really good reason for doing so. Of course, in a full programming language, you can somewhat mask the difference. If I hand the user an object with a few values in it, they don't need to know whether it was constructed from an object literal, computed completely dynamically, or some combination of the two. We can't do this in CSS, and the current planned approaches do indeed require you to commit to one or the other.
As an example of a different possible approach, perhaps the use-case here can solved in a more narrowly-targeted fashion. For example, maybe we can define a "value ramp" (a few types - numeric, color, others) that represents a range of interpolated values, can be passed around in custom properties, queried for particular values on the ramp, and extended by others. Like: /* any number of colors can be given */
--color-primary: color-ramp(in oklch,
100 oklch(95% 13% 135),
900 oklch(25% 20% 135));
/* you can ask the ramp for any value in the range */
background: get-ramp(var(--color-primary), 400);
/* you can create a ramp from an existing one
by adding or overriding stops */
--better-primary: color-ramp(from var(--color-primary),
800 forestgreen); Or maybe the ramp is defined by an at-rule, with the ability to reference custom properties from point-of-use; that might give us a richer syntax to play with, especially for overriding. Like: @color-ramp --primary {
stops: 100 oklch(95% 13% 135),
900 oklch(25% 20% 135);
interpolation: oklch;
}
@color-ramp --better {
extends: --primary;
stops: 800 forestgreen;
}
.foo { background: color-ramp(--primary 400); } This could sprout more abilities, too, like the ability to round the input to some precision (only every 100, or 50, or whatever), define whether it extends past the first/last stops, etc. This sort of approach would also suffer from the "everyone has to use this method" problem you outline, but it's also vastly simpler than "inline functions defined in the declaration grammar", much more extensible, can be specialized to the problem space in important ways, etc. Are all the design-system cases that want generative/continuous values doable with something like this? Are they all just sizes and colors, or other things that might similar fit into this framework? We'd need to see. Maybe they would all just switch to this sort of value, were it provided, so you wouldn't need to worry about some using "a whole bunch of values stored in individual properties" that required a lot of manual renaming. |
I actually agree with this. It's a little uglier, but I think the lack of ambiguity is worth it.
I’m still not super sure what you’re asking. The variables they define today are right there, but they're obviously hardcoded today.
I gave several in #10034. Though this kind of approach does restrict the interpolation tweaking to the design system author, but perhaps that's ok? That said, if we could find a way to allow tweaking from the outside, even better.
Can't disagree with being careful! :) But that applies to everything we do :)
I don't see this as two function syntaxes, in the same way that JS accessors and proxies are not alternative function syntaxes. But these are all questions we should figure out.
I think it may be worth exploring implementability (especially compared to functions). What if shipping dynamic properties first could cover enough use cases that we can then take our time fleshing out functions? But I do think that the use cases are distinct, although there is certainly overlap. In some ways, this is like saying we shouldn't work on
I think the most pervasive pain point is the mass property renaming / passing around. So while I'm not sure if it would be 90% or 80% or 70%, I think addressing that is the bigger priority.
I think decomposing the problem into a bunch of lower level features that can ship independently is often a good path (and what I was trying to do with the alternative decomposed design).
While I agree that it depends on the case, and in many cases you definitely want to expose that. But there are also many legit cases where that is an implementation detail, which is why there are entire programming language features designed to mask exactly that (e.g. accessors and proxies in JS). Also keep in mind that the tokens the design system desires to expose may still be finite — it's the definition that is continuous. In the same way that typeface designers may design a typeface by interpolating between faces, then export a finite number of faces.
This is fascinating, I posted #10034 last night before reading this, and it looks like we’ve been thinking along very similar lines. I agree the piecewise interpolation stuff was not a good fit for this proposal and I have now removed it. Some comments as I read through your proposal: First, I see the benefits an
Wrt overridding the interpolation, there are two use cases here:
I don't see why we'd bake the specific tint levels into the ramp. I think one of the advantages of a rank primitive is to abstract the naming scheme away, and I really like the idea of generating ramps from existing ramps. I think that is very powerful, and does this without having to resort to an
Yes!
Not necessarily. Design systems could still generate aliases that call
I see these as orthogonal problems. Providing better tools for ramps is useful in its own right (which is why I opened #10034). Like I said, I suspect design systems will still alias points along the ramps to variables — now if we give them a way to make this less repetitive in the future, even better! |
When something like this were to be implemented, it would be really nice if whatwg/html#6064 would be available as well. Then you could have: <link rel="stylesheet" supports="<supports-condition-for-var-groups>" href="var-groups-dist.css" />
<link rel="stylesheet" supports="not (<supports-condition-for-var-groups>)" href="transformed-var-groups-dist.css" /> So the browser would choose which one to load by itself; no JS required. |
What are the next steps for this proposal? As a design systems developer, this would really help. |
Sharing this video with permission, as I think it illustrates the pain point better than anything I've ever seen: https://youtu.be/JhfYeXLfWdI?si=vF3xRai2QrjabVFv&t=277 Another use case is rebranding: Shoelace (variables starting with |
This will break existing behavior. I've written an article how to use JSON inside CSS variable. This will break this functionality. There is a need for new syntax for the group (like a new keyword). Not just curly braces that right now is valid CSS custom property value. This is perfectly valid CSS: div {
height: 100vh;
background-image: paint(circle);
--pointer-x: 20px;
--pointer-y: 10px;
--pointer-options: {
"color": "rebeccapurple",
"width": 20
};
} reference: Controlling Paint Worklet with JSON in CSS |
@jcubic Make sure to read the whole discussion, that's been brought up a few times already and is part of the motivation for the |
@adamwathan there are a lot to digest here. There are also different proposals. I just wanted to be sure that this one will not be the part of the spec. |
@jcubic Hey Jakub, nothing is set in stone at this point. As you mentioned, there are several proposals to unpack here and likely concerns to address with each of them, as has been the case for many new CSS features. CSS nesting, for example, went through like 4-5 rounds of revisions, if not more, before settling on the final syntax. As a JS-in-CSS (vs. CSS-in-JS) package author, I leverage the approach you mentioned but could work around any breaking changes resulting from the syntax proposal you called out. Do you have a specific library or package in mind that would be impacted by that syntax, specifically in a way that would affect consumer APIs? Adjusting library/framework APIs under the hood is more manageable and can often flow with API changes. When I started throwing JS into CSS variable values, I knew perfectly well that what I was doing was fringe at best and could be subject to change. That said, as @adamwathan pointed out, this thread seems to be leaning toward the |
@brandonmcconnell No, I don't have any specific library. Just bump into the issue in one of the newsletters (I'm way behind on reading them) and realized that the original proposal will break my demo and article. |
@jcubic I'd love to hear more about your use case. I just DM'd you on X, so we can keep that discussion going offline. |
If the content is valid JSON, properties need to be quoted, so that wouldn't be valid CSS syntax. So depending on how parsing is defined, even the original proposal may not actually break your demo. That said, when we did research on this (for CSS Nesting), the fraction of custom properties whose value was JSON was tiny. |
Sure, but now you can put anything into a CSS variable, including working JavaScript. See this article by Chris Coyer. This is probably not very useful, but someone somewhere may rely on this feature and you may break that user code. |
@jcubic Even with that syntax, it's very likely that the value received by JS for the parent property, |
Sub-property getter/setter syntax
Using a dot separator between levels of grouped properties vs. a hyphen would help disambiguate in cases of naming collisions and provide a clearer method (to developers and parsers) when a property ref is looking at a name vs. entering a group. @mirisuzanne briefly mentioned this syntax here as well in the related "Design of --some-property: var(--foo.bar.baz);
/* as opposed to `get(--foo, bar, baz)` or `get(baz, get(--foo, bar))` */ Disambiguation & naming collisionsUsing a dot syntax would also help to disambiguate grouped property names from ones with hyphen characters: @group --test-* {
a: { b: 1; };
b: { a: 2; };
a-b: 3;
b-a: 4;
} In this example, with hyphen-delimiting, With dot-delimiting, these would not be confusable, as each group level would be explicitly delimited by a
Getter/setter syntax exampleSetting a sub-property might look like this: :root {
/* set up group */
@group --test-* { a: { b: 'abc'; }; a-b: 'xyz'; }
some-selector {
/* override sub-property values */
--test.a.b: 'a.b';
--test.a-b: 'a-b';
/* tests */
--check-grouped: var(--test.a.b); /* a.b */
--check-hyphen: var(--test.a-b); /* a-b */
}
/* tests */
--check-grouped: var(--test.a.b); /* abc */
--check-hyphen: var(--test.a-b); /* xyz */
} @LeaVerou Let me know if you want me to open a new ticket to discuss the getter/setter syntax. |
I would strongly prefer not using an
I’m also not a fan of the dot syntax. One of the goals of this proposal was to pave the cowpaths and facilitate the patterns that are already in wide use, not introduce a new syntax pattern that needs to gain traction. Also, I think it’s a good design goal that it should not matter how a design token was defined (whether it was a group or an explicit variable declaration) at the point of usage. I.e. my component should neither need to know nor care how you defined your design tokens. |
@LeaVerou I would also prefer not to introduce an extra @-rule like My main focus was the dot syntax, though I understand your feedback and concern there too. Sub-groups/propertiesFor this example… --test-* {
a: {
b: {
base: 'abc';
extra: 'def';
}
};
a-b: 'xyz';
} Without an alternative getter/setter syntax like the dot syntax…
Getter / setter syntax
|
I've played around some with stuff weird values into custom properties and these questions are related to that experience. Currently, using a variable inside curly braces doesn't compute properly, e.g. a rule with You also noted that since groups define constituent properties, they compose with individual ones, but I'm wondering how this works if I want to override only part of a group (and sorry if this was covered somewhere)? body {
--p {
1: 1px;
}
--p-2: 2px;
}
div {
--p: {
3: 3px;
}
} On |
The whole proposal is here, I have included a summary below.
Pain points (summary)
The main pain point is aliasing. Currently, design systems are specified as a long series of variables, e.g.
--color-green-100
,--color-crimson-950
etc.Aliasing them to other names (e.g. semantic names such as
--color-primary
or simply shorter/simpler names) is currently author hostile, as it requires manually aliasing every single one, i.e. hundreds of declarations. Not only is this painful to do for the whole page, it makes it very hard to theme areas on the page with a different color, or pass design tokens to components.Additionally, tints and shades for each color are generated manually, even if some of them could be inferred via interpolation. Theoretically that can already be done via a series of
color-mix()
values, but it is very tedious and repetitive, especially if you want to tweak the endpoints by adding more manual values where the interpolated ones don't work well.Other ideas explored
The following are useful in their own right, but I don’t think solve the pain points equally well.
The proposal (summary)
At its core (and as an MVP) this proposal allows authors to define groups of variables with the same prefix by using braces, and then pass the whole group around to other variables.
The granularity is completely progressive: you can go from defining
--ds-color-green
as a group, to defining--ds-color
as a group to defining--ds
as a group without changing anything about the places referencing them.--color-green-100
will override the inherited one from--color-green
(just like with shorthands).Using groups on non-custom properties: The
base
propertyAuthors can optionally specify a
base
value, which will be output if the custom property is used in a context that does not support groups, such as a regular CSS property.This can also be tweaked from outside the group (just like the group properties), by setting
--color-green-base
. This also facilitates the use case of having constituent properties that are dynamically computed from the base so changing the base tweaks all of them in unison.Continuous tokens
(This is definitely beyond MVP and more speculative)
Authors can specify a catch-all expression to be used to resolve undefined keys via a
default
property:Together with something like
color-scale()
this could also facilitate piecewise interpolation.Alternative decomposed design
We could decouple this into three separate features:
--color-primary: group(--color-green)
or evenvar()
itself, with a*
to mark the prefix:--color-primary-*: var(--color-green-*);
, which would require handling base values manually (but based on the comments, that seems to be seen as a feature for some).--color-green-*
or using a syntax to mark the key part like--color-green-[tint]
(which would also allow for multiple arguments in the future). The syntax space here is likely quite restricted due to the restrictions we had to introduce to the grammar to make&
-less nesting work.I ran this by a couple design systems folks I know, and the response so far has been overwhelmingly "I NEED THIS YESTERDAY". While I’m pretty sure the design can use a lot of refinement and I’m not sure about the technical feasibility of some of these ideas, I’m really hoping we can prioritize solving this problem.
Note that beyond design systems, this would also address many (most?) of the use cases around maps that keep coming up (don't have time to track them down right now, but maybe someone else can).
Before commenting please take a look at the whole proposal in case the issue you’re about to point out has been addressed there!
The text was updated successfully, but these errors were encountered: