CSS relative color syntax

Create new colors based on another color's channels and values.

Adam Argyle
Adam Argyle

In Chrome 119 is a very powerful color feature from CSS Color Level 5. Relative color syntax creates a smooth path for color manipulation within CSS, offering ways for authors and designers to:

Before relative color syntax, to modify the opacity of a color, you need to create custom properties for the channels of a color, usually HSL, and assemble them into a final color and final variant color. This means managing a lot of color pieces, which can quickly become burdensome.

:root {
  --brand-hue: 300deg;
  --brand-saturation: 75%;
  --brand-lightness: 50%;

  --brand-hsl:
    var(--brand-hue)
    var(--brand-saturation)
    var(--brand-lightness);

  --brand-color: hsl(var(--brand-hsl));

  /* all this work just so I can set the opacity to 50% in a variant */
  --brand-color-variant: hsl(var(--brand-hsl) / 50%);
}

After relative color syntax, you can create a brand color with any color space or syntax you need, and create a half opacity variant with much less code. It's also much easier to read the intent of the styles and system.

:root {
  --brand-color: hsl(300deg 75% 50%);
  --brand-color-variant: hsl(from var(--brand-color) h s l / 50%);
}

This post will help you learn the syntax and demonstrate common color manipulations.

If you prefer video, nearly all of the following article is covered in this GUI Challenge.

Syntax overview

The goal of relative color syntax is to allow deriving a color from another color. The base color is called the originating color, this is the color that comes after the new from keyword. The browser will convert and break this originating color apart and offer the parts as variables for use in the new color definition.

A
diagram of the syntax rgb(from green r g b / alpha) is shown, with an arrow
leaving the top of green and arching into the rgb beginning of the function,
this arrow splits into 4 arrows that then point to their relevant variable. The
4 arrows are red, green, blue and alpha. Red and blue have a value of 0, green
is 128 and alpha is 100%.

The preceding diagram shows the originating color green being converted to the new color's color space, turned into individual numbers represented as r, g, b, and alpha variables, which are then directly used as a new rgb() color's values.

While this image shows the breakdown, process and variables, it's also not changing the color. The variables are put back into the color unchanged, thus resulting in a green color still.

The from keyword

The first part of the syntax to learn is the part from <color> addition to specifying a color. It goes right before you specify the values. Here's a code example where all that has been added is from green, right before the values for rgb() are specified.

.syntax-introduction_same-colors {
  color: green;
  color: rgb(0 128 0);
  color: rgb(from green r g b);    /* result = rgb(0 128 0) */
}

That from keyword, when seen as the first parameter in functional notation, turns the color definition into a relative color! After the from keyword, CSS expects a color, a color that will inspire the next color.

Color conversion

In simpler terms, it converts green to r g and b channels for use in a new color.

rgb(from green r g b)           /* r=0 g=128 b=0 */
rgb(from rgb(0 128 0) r g b);   /* r=0 g=128 b=0 */

Colors from custom properties

Reading rgb from green is very clear and easy to understand. This is why custom properties and relative color syntax make such a great match, because you can take the mystery out of the from color. You also don't generally need to know the color format of the custom property color, as you're creating a new color in a format of your choice.

rgb(from rgb(255 105 180) r g b) /* ????? */
rgb(from var(--hotpink) r g b)   /* clear */

Work in your preferred color space

You can choose the color space with your choice of functional color notation.

rgb(from hsl(120 100% 25%) r g b)     /*  r=0   g=128  b=0    */
hsl(from hsl(120 100% 25%) h s l)     /*  h=120 s=100% l=25%  */
hwb(from hsl(120 100% 25%) h w b)     /*  h=120 w=0%   b=50%  */
lch(from hsl(120 100% 25%) l c h)     /*  l=46  c=68   h=134  */

The relative color syntax has that conversion step; the color after from is converted into the color space as specified at the beginning of the relative color. The input and output don't need to match, which is very liberating.

The ability to choose a color space is also empowering, as choosing a color space tends to be more focused around the type of color alternation than it is a preference. The preference is in the results, not the color format or channel types. This will become much clearer in the sections demonstrating use cases, as different color spaces excel at different tasks.

Mix, match, omit and repeat the variables

Something strange but exciting about this syntax, the variables don't have to be put back in order and can be repeated.

rgb(from green g g g)    /* rgb(128 128 128) */
rgb(from green b r g)    /* rgb(0 0 128) */
rgb(from green 0 0 g)    /* rgb(0 0 128) */

Opacity as a variable

The syntax also provides the opacity as a variable named alpha. It's optional, and goes after the / in the functional color notation.

rgb(from #00800080 r g b / alpha)             /* alpha=50% */
rgb(from rgba(0,128,0,.5) r g b / alpha)      /* alpha=50% */
rgb(from rgb(0 128 0 / 50%) r g b / alpha)    /* alpha=50% */

Use calc() or other CSS functions on the variables

So far we've been creating the color green over and over again. Learning the syntax, getting familiar with the conversion and destructuring steps. Now is time to modify the variables, alter the output so it's not the same as the input.

green                              /*  h=120 s=100% l=25%  */
hsl(from green calc(h * 2) s l)    /*  h=240 s=100% l=25%  */

It's navy now! The hue was doubled, taking a hue of 120 and turning it into 240, completely altering the color. This rotated the hue along the color wheel, a neat trick made very simple with cylindrical color spaces like HSL, HWB, LCH, and OKLCH.

To visually see the values of channels, so you can get the math just right without guessing or having the spec memorized, try this relative color syntax channel values tool. It reveals each channels value based on the syntax you specify, allowing you to know exactly what values you have available to play with.

Check for browser support

@supports (color: rgb(from white r g b)) {
  /* safe to use relative color syntax */
}

Use cases and demonstrations

The following examples and use cases have many alternative syntaxes to achieve similar or the same results. The variations come from the color spaces and the channels they offer.

Also, many examples will show color adjustments with the verbiage of by and to. A color changed by is a relative color change; a change that uses the value of the variable and makes an adjustment based on its current value. A color changed to is an absolute color change; a change that does not use the value of the variable and instead specifies a completely new value.

All demo's can be found in this Codepen collection.

Lighten a color

The OKLCH, OKLAB, XYZ or sRGB color spaces provide the most predictable results when lightening colors.

Lighten by an amount

The following example .lighten-by-25 takes the color blue and converts it to OKLCH, then lightens the blue by increasing the l (lightness) channel by multiplying the current value by 1.25. This pushes the blue lightness towards white by 25%.

.lighten-by-25 {
  background: oklch(from blue calc(l * 1.25) c h);
}

Lighten to a specific value

The following example .lighten-to-75 doesn't utilize the l channel to lighten blue, it instead completely replaces the value with 75%.

.lighten-to-75 {
  background: oklch(from blue 75% c h);
}

Darken a color

The same color spaces effective at lightening a color, are also great for darkening color.

Darken by an amount

The following example .darken-by-25 takes the color blue and converts it to OKLCH, then darkens the blue by decreasing the l (lightness) channel by 25% by multiplying the value by .75. This pushes the blue color towards black by 25%.

.darken-by-25 {
  background: oklch(from blue calc(l * .75) c h);
}

Darken to a specified value

The following example .darken-to-25 doesn't utilize the l channel to darken blue, it instead completely replaces the value with 25%.

.darken-to-25 {
  background: oklch(from blue 25% c h);
}

Saturate a color

Saturate by an amount

The following example .saturate-by-50 uses the s from hsl() to increase orchid's vibrance by a relative 50%.

.saturate-by-50 {
  background: hsl(from orchid h calc(s * 1.5) l);
}

Saturate to a specific amount

The following example .saturate-to-100 doesn't utilize the s channel from hsl(), it instead specifies a desired saturation value. In this example, saturation is raised to 100%.

.saturate-to-100 {
  background: hsl(from orchid h 100% l);
}

Desaturate a color

Desaturate by an amount

The following example .desaturate-by-half uses the s from hsl() to decrease the saturation of indigo by half.

.desaturate-by-half {
  background: hsl(from indigo h calc(s / 2) l);
}

Desaturate to a specific value

Rather than desaturate by an amount, you can desaturate to a specific desired value. The following example .desaturate-to-25 creates a new color based on indigo but sets the saturation to 25%.

.desaturate-to-25 {
  background: hsl(from indigo h 25% l);
}

Chroma boost a color

This effect is similar to saturating a color but is different in a couple of ways. Firstly, it's a chroma change and not a saturation change, and this is because the color spaces that can boost into high dynamic range don't use saturation. The color spaces which feature chroma are high dynamic range capable, allowing authors to boost color vibrance further than saturation is even capable of.

.increase-chroma {
  background: oklch(from orange l calc(c + .1) h);
}

Adjust opacity a color

Making a semi-transparent variant of a color is one of the most common color adjustments done in design systems. See the example in the introduction of this article if you missed it, it outlines the problem space really well.

Adjust opacity by an amount

.decrease-opacity-by-25 {
  background: rgb(from lime r g b / calc(alpha / 2));
}

Adjust opacity to a specific value

.decrease-opacity-to-25 {
  background: rgb(from lime r g b / 25%);
}

Invert a color

Color inversion is a common color adjustment function found in color libraries. One way to accomplish this is to convert a color to RGB then subtract each channel's value from 1.

.invert-each-rgb-channel {
  background: rgb(from yellow calc(255 - r) calc(255 - g) calc(255 - b));
}

Complement a color

If your goal wasn't to invert a color but rather complement it, then hue rotation is likely what you're looking for. Pick a color space which offers the hue as an angle, then use calc() to rotate the hue by an amount you want. Finding a color's complement is done by rotating by half a turn, in this case you can add or subtract from the h channel by 180 to achieve the result.

.complementary-color {
  background: hsl(from blue calc(h + 180) s l);
}

Contrast a color

As a method of achieving accessible color contrast ratios, consider L* (Lstar). This uses the (approximately) perceptually uniform lightness (L) channel from LCH and OKLCH, in a calc(). Depending if you are targeting low, medium or high contrast, the L* delta is around ~40, ~50, or ~60.

This technique works well across any hue in LCH or OKLCH.

Contrast a darker color

The .well-contrasting-darker-color class demonstrates L* with a delta of 60. Since the originating color is a dark color (low value lightness), 60% (.6) is added to the lightness channel. This technique is used to find a well contrasting, same hue, dark text color on a light background.

.well-contrasting-darker-color {
  background: darkred;
  color: oklch(from darkred calc(l + .60) c h);
}

Contrast a lighter color

The .well-contrasting-lighter-color class demonstrates L* with a delta of 60% also. Since the originating color is a light color (high value lightness), .60 is subtracted from the lightness channel.

.well-contrasting-lighter-color {
  background: lightpink;
  color: oklch(from lightpink calc(l - .60) c h);
}

Color palettes

Relative color syntax is very good at creating color palettes. It's especially useful and powerful due to the number of color spaces available. The following examples all use OKLCH because the lightness channel is reliable and the hue channel can be rotated without side effects. The final example demonstrates a combination of lightness and hue rotation adjustments for a more interesting result!

Open the example source code for these and try changing the --base-color, to see just how dynamic these palettes are. It's fun!

If you like video, I give in depth information about building color palettes in CSS with OKLCH on YouTube.

Monochromatic palettes

To create a monochromatic palette is to make a palette all from the same hue but with variations in lightness and darkness. The middle color is the source color for the palette, where two lighter and two darker variants are put on either side.

:root {
  --base-color: deeppink;

  --color-0: oklch(from var(--base-color) calc(l + .20) c h); /* lightest */
  --color-1: oklch(from var(--base-color) calc(l + .10) c h);
  --color-2: var(--base-color);
  --color-3: oklch(from var(--base-color) calc(l - .10) c h);
  --color-4: oklch(from var(--base-color) calc(l - .20) c h); /* darkest */
}
Try out a bunch of palettes made with relative color syntax and OKLCH

Open Props, a library of free CSS variables, offers color palettes built with this strategy and makes them easily usable with an import. They're also all built off of a color you can customize, you just give it a color and it spits out a palette!

Analogous palettes

Since hue rotation is so easy with OKLCH and HSL, it's trivial to create an analogous color palette. Rotate the hue by an amount you like the results of and change the base color, and watch new palettes get built by the browser.

:root {
  --base-color: blue;

  --primary:   var(--base-color);
  --secondary: oklch(from var(--base-color) l c calc(h - 45));
  --tertiary:  oklch(from var(--base-color) l c calc(h + 45));
}

Triadic palettes

Similar to complementary colors, triadic color palettes are opposing but harmonious hue rotations given a base color. Where a complementary color is on the opposite side of a color, like a straight line drawn through the middle of the color wheel, triadic palettes are like a triangle of lines, finding 2 colors equally rotated from a base color. Accomplish this by rotating the hue 120deg.

This is a slight simplification of the color theory, but it's enough to kick start you into the more complex triadic palettes if you're interested.

:root {
  --base-color: yellow;
  --triad-1: oklch(from var(--base-color) l c calc(h - 120));
  --triad-2: oklch(from var(--base-color) l c calc(h + 120));
}

Tetradic palettes

Tetradic palettes are four colors evenly divided around the color wheel, making a palette with no clear dominant value. You could think of it too, like two pairs of complementary colors. Used wisely, it can be very meaningful.

This is a slight simplification of the color theory, but it's enough to kick start you into the more complex tetradic palettes if you're interested.

:root {
  --base-color: lime;

  --color-1: var(--base-color);
  --color-2: oklch(from var(--base-color) l c calc(h + 90));
  --color-3: oklch(from var(--base-color) l c calc(h + 180));
  --color-4: oklch(from var(--base-color) l c calc(h + 270));
}

Monochromatic with a slight hue rotation

Many color experts keep this trick up their sleeve. The problem is, a monochromatic color scale can be quite boring. The solution is to add either a minor or major hue rotation to each new color as the lightness is changed.

The following example decreases lightness by 10% each swatch and also rotates the hue by 10 degrees. The result, a hotpink to indigo palette that seems to seamlessly blend like a gradient might.

:root {
  --base-color: deeppink;

  --color-1: var(--base-color);
  --color-2: oklch(from var(--base-color) calc(l - .10) c calc(h - 10));
  --color-3: oklch(from var(--base-color) calc(l - .20) c calc(h - 20));
  --color-4: oklch(from var(--base-color) calc(l - .30) c calc(h - 30));
  --color-5: oklch(from var(--base-color) calc(l - .40) c calc(h - 40));
}
Try out this leaderboard built with OKLCH and hue rotation

The following leaderboard interface uses this hue rotation strategy. Each list item tracks its index in the document as a variable called --i. This index is then used to adjust chroma, lightness and hue. The adjustment is only by 5% or 5deg, much more subtle than the above example with deeppink, and so it takes a keen eye to notice the reason this leaderboard can be in any hue with such elegance.

Be sure to change the hue in the slider underneath the leaderboard, and see relative color syntax create beautiful color moments.

li {
  --_bg: oklch(
    /* decrease lightness as list grows */
    calc(75% - (var(--i) * 5%))

    /* decrease chroma as list grows */
    calc(.2 - (var(--i) * .01))

    /* lightly rotate the hue as the list grows */
    calc(var(--hue) - (var(--i) + 5))
  );
}