You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There are many use cases that require doing math on components from more than one color, and this is currently impossible without having separate variables for each component.
Example use cases
In the following I'll use an extension of RCS that supports additional colors via the same idents with a number after their name (e.g. c2 for the second color's chroma while the first one remains c). The next section contains a more detailed syntax discussion.
Note
Yes, many of these would be better solved with higher level features that are more specific to the use case. However, the argument I'm making is that this is a low-level feature that makes many use cases possible, giving us more time to make them easy, which was also a big motivation behind RCS itself.
1. Combining components from multiple colors
Lightness from one and hue & chroma from another
--color-accent-80:lch(from accentColor var(--color-blue-80) l2 c h);
Applying the same ratio of chromas would take 3 colors (using blue as a sort of "template" for the chroma ratio)
We've also had several use cases for combining color components from one color and alpha from another but I can't find them right now, one was even high priority as it was needed for a11y. Does anyone have a link handy?
Custom contrasting text color
This also came up when generating text colors automatically. Both this trick, as well as contrast-color() generate white and black, but in reality you rarely want black (or even "a very dark color"), you want one of your actual design tokens! white is often acceptable as the light color, but black (or even "a very dark color") rarely is.
--l:clamp(0, (l /var(--l-threshold,0.645) -1) * -infinity,1);
color:oklch(from var(--color) var(--l) 0 h);
/* or, once contrast-color is a thing: */color:contrast-color(var(--color) max);
With multiple colors, you could do (picking between white and a dark color:
--color-bw: color: contrast-color(var(--color) max);
--p:round(progress(l,0,1)); /* round to either 0 or 1 */color:oklch(
from var(--color-bw) var(--color-dark) var(--color-light)
calc-mix(var(--p), l2, l3) calc-mix(var(--p), h2, h3) calc-mix(var(--p), h2, h3)
);
If the "repeating the list" idea from below is implemented, the same formula could be used with either 2 or 3 colors, and would just fall back to white if no light color is specified and to white and black if only one color is specified.
Implementing light-dark() (if it didn't exist)
If light-dark() were not a thing, the same formula could be used for that too, to pick one of two colors based on whose lightness was closest to that of canvas (or canvastext):
--color-bw: color: contrast-color(var(--color) max);
--p:round(progress(l,0,1)); /* round to either 0 or 1 */color:oklch(
from canvas var(--color-dark) var(--color-light)
calc-mix(var(--p), l2, l3) calc-mix(var(--p), h2, h3) calc-mix(var(--p), h2, h3)
);
Interpolation at a different rate per component
Example: Generating intermediate tints from an accent color and its lightest tint (assumes #11530 is accepted), where chroma typically interpolates at a different rate than other components:
:root {
--lightnesses:0,0.18,0.24,0.33,0.4,0.47,0.57,0.68,0.76,0.84,0.92,0.96,100;
/* Tint that contains the accent color, can be reused across hues */--accent-tint:round(progress(l,var(--lightnesses)),5);
/* Progress of accent tints towards lightest tint, can be reused across hues */--tint-progress-80:progress(80,var(--accent-tint),95);
/* Math for figuring out components for tint 80, can be reused across hues */--tint-80:var(--l-80) calc-mix(pow(var(--tint-progress-80),2), c, c2) h;
}
.accented {
--color-80:lch(from var(--color) var(--color-95) var(--tint-80));
}
While this may seem complicated, it could be an immense help for #10948.
A design system with the average of 14 scales and 11 tints per hue (source) needs to define 14 * 11 = 154 custom properties, and to pass a color to a component or to change the color of a given element/subtree, one needs to set 11 custom properties. Being able to generate even just the intermediate ones would reduce these to just 3, a 72% reduction.
Implementing two color operations, e.g. blending modes
I've often needed operations like multiply on individual colors. Sure, if the need is widespread we could introduce an explicit function, but meanwhile, something as low-level like this allows authors implementing their own (and possibly shipping libraries with entire sets of custom properties for such operations):
Once device-cmyk() actually ships, this can be used for overprint too:
--cmyk-overprint:clamp(none, c + c2,100%) clamp(none, m + m2,100%) clamp(none, y + y2,100%) clamp(none, k + k2,100%);
background:device-cmyk(from var(--color-1) var(--color-2) var(--cmyk-overprint));
Syntax
Assuming we have consensus that the problem needs solving, how do we solve it?
Some would argue it should be solved in color-mix(). I disagree. I think that would make for a much more cumbersome syntax, and is not easily extensible to >2 colors. It would also likely restrict use cases.
In #6937 we resolved to add color-extract() but that is a more general function, and would result in a lot of verbosity. Also, without restricting it to be used only within color functions, I suspect it could raise security concerns which would slow down implementation even more.
I think the nicest solution would be to extend RCS to support multiple colors by simply changing from <color> to from <color>+ in its grammar. This may even obliterate the need for color-extract() altogether — we should revisit it after to see if there are any remaining use cases for it.
Then the question becomes: how do we reference components of the 2nd, 3rd etc color? Some options are:
Generate idents like c2, c3 etc. Or perhaps c-2, c-3 etc.
I suspect some people may be more comfortable with a functional syntax like c(2) rather than supporting arbitrary idents. @fantasai and I are not huge fans of the extra parens (we already have too many!) but in the interest of moving the proposal forwards, I would not object to it. One advantage of it would be that it would support variables for the color index without depending on [css-values] A way to dynamically construct custom-ident and dashed-ident values #9141, though that's a small advantage since that's almost certainly shipping before this proposal. 😁
Another option would be to pass the decision onto the user, by requiring them to name either the extra colors (and using that as a suffix) or the components. However, both @fantasai and I thought that this would add extra friction and the vast majority of cases would just be to add a numerical index like the one discussed in 1. We could ship a way to name these colors later, as an optional customization for nicer expressions, but IMO it should not be mandatory.
Sugar
A nice-to-have would be to also support a 1 version for the first color, i.e. c1/c-1/c(1) becomes an alias of c.
Another question is, how to deal with components out of bounds? E.g. c2 being specified when only one color is used. We could treat it as invalid and that would probably be fine. However, I think a better solution that allows more flexibility would be to resolve it against the color list we do have:
If only one color is specified, it resolves to the corresponding component of that color
If M (M > 1) colors are specified, getting a component of the n-th color (n > M) would be an alias to the corresponding component of the k-th color, where k = n mod M, i.e. extend the list of colors by repeating it.
This way, we can write expressions that account for up to e.g. N colors but fall back gracefully to fewer colors, which can be useful for use cases where we want alternating colors like e.g. charts, syntax highlighting, accented sections etc. See the contrast color use case above for an example of how this could help simplify code.
Layering
If it makes things easier for implementors, shipping a version that only supports up to 3 or even just 2 colors at first would still cover the vast majority of use cases (and the rest can be done by nesting multiple of these). Personally, I don't think I've ever encountered a use case that needed more than 3, actually.
Even in the long run, I think it's fine to set a relatively low upper bound (e.g. 16) for the number of colors that can be specified.
And obviously the sugar above could also be a Level 2 thing (as long as values out of bounds are treated as an error).
The text was updated successfully, but these errors were encountered:
There are many use cases that require doing math on components from more than one color, and this is currently impossible without having separate variables for each component.
Example use cases
In the following I'll use an extension of RCS that supports additional colors via the same idents with a number after their name (e.g.
c2
for the second color's chroma while the first one remainsc
). The next section contains a more detailed syntax discussion.Note
Yes, many of these would be better solved with higher level features that are more specific to the use case. However, the argument I'm making is that this is a low-level feature that makes many use cases possible, giving us more time to make them easy, which was also a big motivation behind RCS itself.
1. Combining components from multiple colors
Lightness from one and hue & chroma from another
Applying the same ratio of chromas would take 3 colors (using
blue
as a sort of "template" for the chroma ratio)We've also had several use cases for combining color components from one color and alpha from another but I can't find them right now, one was even high priority as it was needed for a11y. Does anyone have a link handy?
Custom contrasting text color
This also came up when generating text colors automatically. Both this trick, as well as
contrast-color()
generatewhite
andblack
, but in reality you rarely want black (or even "a very dark color"), you want one of your actual design tokens!white
is often acceptable as the light color, but black (or even "a very dark color") rarely is.With multiple colors, you could do (picking between
white
and a dark color:or, to customize both the light and dark color:
If the "repeating the list" idea from below is implemented, the same formula could be used with either 2 or 3 colors, and would just fall back to white if no light color is specified and to white and black if only one color is specified.
Implementing
light-dark()
(if it didn't exist)If
light-dark()
were not a thing, the same formula could be used for that too, to pick one of two colors based on whose lightness was closest to that ofcanvas
(orcanvastext
):Interpolation at a different rate per component
Example: Generating intermediate tints from an accent color and its lightest tint (assumes #11530 is accepted), where chroma typically interpolates at a different rate than other components:
While this may seem complicated, it could be an immense help for #10948.
A design system with the average of 14 scales and 11 tints per hue (source) needs to define 14 * 11 = 154 custom properties, and to pass a color to a component or to change the color of a given element/subtree, one needs to set 11 custom properties. Being able to generate even just the intermediate ones would reduce these to just 3, a 72% reduction.
Implementing two color operations, e.g. blending modes
I've often needed operations like
multiply
on individual colors. Sure, if the need is widespread we could introduce an explicit function, but meanwhile, something as low-level like this allows authors implementing their own (and possibly shipping libraries with entire sets of custom properties for such operations):Once
device-cmyk()
actually ships, this can be used for overprint too:Syntax
Assuming we have consensus that the problem needs solving, how do we solve it?
Some would argue it should be solved in
color-mix()
. I disagree. I think that would make for a much more cumbersome syntax, and is not easily extensible to >2 colors. It would also likely restrict use cases.In #6937 we resolved to add
color-extract()
but that is a more general function, and would result in a lot of verbosity. Also, without restricting it to be used only within color functions, I suspect it could raise security concerns which would slow down implementation even more.I think the nicest solution would be to extend RCS to support multiple colors by simply changing
from <color>
tofrom <color>+
in its grammar. This may even obliterate the need forcolor-extract()
altogether — we should revisit it after to see if there are any remaining use cases for it.Then the question becomes: how do we reference components of the 2nd, 3rd etc color? Some options are:
c2
,c3
etc. Or perhapsc-2
,c-3
etc.c(2)
rather than supporting arbitrary idents. @fantasai and I are not huge fans of the extra parens (we already have too many!) but in the interest of moving the proposal forwards, I would not object to it. One advantage of it would be that it would support variables for the color index without depending on [css-values] A way to dynamically construct custom-ident and dashed-ident values #9141, though that's a small advantage since that's almost certainly shipping before this proposal. 😁Sugar
A nice-to-have would be to also support a
1
version for the first color, i.e.c1
/c-1
/c(1)
becomes an alias ofc
.Another question is, how to deal with components out of bounds? E.g.
c2
being specified when only one color is used. We could treat it as invalid and that would probably be fine. However, I think a better solution that allows more flexibility would be to resolve it against the color list we do have:This way, we can write expressions that account for up to e.g. N colors but fall back gracefully to fewer colors, which can be useful for use cases where we want alternating colors like e.g. charts, syntax highlighting, accented sections etc. See the contrast color use case above for an example of how this could help simplify code.
Layering
If it makes things easier for implementors, shipping a version that only supports up to 3 or even just 2 colors at first would still cover the vast majority of use cases (and the rest can be done by nesting multiple of these). Personally, I don't think I've ever encountered a use case that needed more than 3, actually.
Even in the long run, I think it's fine to set a relatively low upper bound (e.g. 16) for the number of colors that can be specified.
And obviously the sugar above could also be a Level 2 thing (as long as values out of bounds are treated as an error).
The text was updated successfully, but these errors were encountered: