Description
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.
- Custom functions
- CSS functions for tints and shades
- Dedicated syntax for design tokens with standardized names for the low level common things
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.
/* Author CSS */
:root {
--color-green: {
100: oklch(95% 13% 135);
200: oklch(95% 15% 135);
/* ... */
900: oklch(25% 20% 135);
};
}
some-component {
--color-primary: var(--color-green);
}
/* some-component.css */
:host {
background: var(--color-primary-100);
border: var(--color-primary-400);
color: var(--color-primary-900);
}
- Groups can be infinitely nested, to specify not only tints of a single color, but an entire color palette or even an entire design system and pass it around with a single CSS variable.
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. - Individual properties can still be specified outside of the group and overridden via regular CSS cascade rules. This provides a migration path for porting existing design systems to groups without their code having to be updated.
- Groups (and their constituent properties) inherit normally, like any other value. Custom properties in descendants can shadow inherited properties with the same name, i.e. a local
--color-green-100
will override the inherited one from--color-green
(just like with shorthands). - There may be value in exposing a functional syntax for every CSS variable group, to facilitate selecting
Using groups on non-custom properties: The base
property
Authors 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.
--color-green: {
base: oklch(65% 50% 135);
100: oklch(95% 13% 135);
200: oklch(95% 15% 135);
/* ... */
900: oklch(25% 20% 135);
};
.note {
/* Same as border: 1px solid oklch(65% 50% 135)); */
border: 1px solid var(--color-green);
}
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:
--color-green: {
base: oklch(65% 50% 135);
100: oklch(95% 13% 135);
default: color-mix(in oklch, var(--color-green-100) calc((100 - arg / 10) * 1%), var(--color-green-900));
900: oklch(25% 20% 135);
};
Together with something like color-scale()
this could also facilitate piecewise interpolation.
Alternative decomposed design
We could decouple this into three separate features:
- A function to map CSS variables with a common prefix to a different prefix, e.g.
--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). - A nesting syntax for setting multiple variables with the same prefix at once. This would look just like the one above, but instead of specifying a group, it is just syntactic sugar for setting many variables at once.
- Continuous variations would have to be specified as a separate feature, perhaps by setting
--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!