Description
During the discussion in #9992 it occurred to me that one of the things that could really help simplify the color-related design systems use cases would be a way to define a gradient line and pick a color on it.
Why?
- As a primary use case, authors often need to define scales of colors with interim colors inserted to adjust the interpolation, and
color-mix()
is not very friendly to that. Super common example: the failure-success scale of red, orange, yellow, green. Yelp ratings are a popular application (and no, this is not just a simple polar interpolation between the endpoints): - Design systems could then define color scales for tints or semi-transparent variants and pass them around in variables, which would make functions a much more appealing solution for actually selecting points on those scales.
- It makes interpolation with arbitrary manual interim points much easier, without requiring special overfit syntax in variable groups to cater to this.
- The scale specification should be compatible with gradient color stops so that authors can debug them by simply throwing them into a
linear-gradient()
.
Syntax
Option 1: Single function for both defining the scale, and selecting a color
<color-scale()> = color-scale ( <percentage> / <color-interpolation-method>?, <abstract-color-stop-list> )
<abstract-color-stop-list> = <abstract-color-stop> , [ <abstract-color-hint>? , <abstract-color-stop> ]#
<abstract-color-stop> = <color> <percentage>?
Example usage:
--tints-green: white, var(--color-green), black;
--color-green-900: color-scale(90% / var(--tint-green));
This is basically modeled after linear-gradient()
with the non relevant parts removed (lengths, to <whatever>
, angles).
It could also allow <1d-image>
/ stripes()
to facilitate discrete scales.
The reason the percentage is separated from the rest with a /
is to facilitate storing the actual scale part into variables and passing them around without having to worry about whether you need to specify a comma or not (depending on whether the scale has a <color-interpolation-method>
).
Pros:
- By passing a list of arguments around, these can produce both a color scale and various types of gradients (without gradients having to be extended in any way)
- Color stop list could even be extended by adding more stops on either side
Cons: - Scale variables don't make sense by themselves, as they're just a comma-separated list of colors.
Option 2: Separate scale definition and color selection
This syntax decouples the scale from the color selection.
It seems more conceptually sound, but also too many parens.
<color-scale> = color-scale ( <color-interpolation-method>?, <abstract-color-stop-list> )
<abstract-color-stop-list> = <abstract-color-stop> , [ <abstract-color-hint>? , <abstract-color-stop> ]#
<abstract-color-stop> = <color> <percentage>?
<color-pick()> = color-pick(<percentage> of <color-scale>);
Example:
--tints-green: color-scale(white, var(--color-green), black);
--color-green-900: color-pick(90% of var(--tints-green));
the parens could be reduced if it would be possible to define tokens like:
<color-scale-color> = <percentage> of <color-scale>
Example:
--tints-green: color-scale(white, var(--color-green), black);
--color-green-900: 90% of var(--tints-green);
but I suspect @tabatkins will have a good reason to rule that out 😁
We could also make it a variant of color-mix()
:
<color-mix> := color-mix(<percentage> of <color-scale>)
Example:
--tints-green: color-scale(white, var(--color-green), black);
--color-green-900: color-mix(90% of var(--tints-green));
Though since conceptually we're not mixing anything, I don't think this is worth it.
More Examples
Yellow tints that skew oranger when darker
Option 1:
--color-yellow: oklch(88% 0.2 95);
--color-yellow-100: oklch(99% 0.03 100);
--color-yellow-900: oklch(40% 0.09 70);
--tints-yellow: var(--color-yellow-100), var(--color-yellow), var(--color-yellow-900);
--color-green-200: color-scale(20% / var(--tints-yellow));
Transparent variations of a base color
Option 1:
--color-neutral-a: var(--color-neutral), transparent;
--color-neutral-a-90: color-scale(90% / var(--color-neutral-a));
Option 2:
--color-neutral-a: color-scale(var(--color-neutral), transparent);
--color-neutral-a-90: 90% of var(--color-neutral-a));
Success/failure scales
This is super common to communicate varying levels of success/failure.
There are two main forms: red - orange - yellow - green - dark green, or red - white - green.
E.g. see screenshot from Coda’s conditional formatting:

Especially the red - orange - yellow - green scales almost always require manual correction, and cannot be done with a single 2 color interpolation (yes not even in polar spaces).
With color-scale()
they can be as simple as:
:root {
--color-scale-bad-good: in oklch, var(--red), var(--orange), var(--yellow) 50%, var(--green) 90%, var(--dark-green);
}
.badge {
background: color-scale(var(--percent-good) of var(--color-scale-bad-good));
.great { --percent-good: 100% }
.good { --percent-good: 80% }
.ok { --percent-good: 60% }
.fair { --percent-good: 40% }
.poor { --percent-good: 20% }
.terrible { --percent-good: 0% }
}