Skip to content

[css-color-5] color-scale() for creating color scales via interpolation #10034

Open
@LeaVerou

Description

@LeaVerou

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):
    • image
    • image
    • image
    • image
    • image
    • image
  • 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:

image

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% } 
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions