css-colores-relativos-color-mix

Relative Colors in CSS with color-mix and Modern Syntax

  • 7 min

A relative color in CSS is a color calculated from another existing color.

Until now, we’ve written colors as closed values: #2563eb, rgb(37 99 235), or hsl(217 91% 60%). That works fine, but in a real interface, we almost never use a single isolated color. We need lighter, darker, more transparent, or slightly less saturated variants.

For a long time, those variants were calculated by hand or with external tools. We’d pick a main blue, open a palette, copy five similar shades, and paste them into CSS as if we were taking inventory of paints.

Modern CSS allows us to do something much more convenient: start from a base color and generate other colors directly from CSS.

The problem of duplicate palettes

Suppose we have a main color for our brand.

:root {
    --color-primary: #2563eb;
}
Copied!

Now we want to use this color on a button, a soft border, a light background, and a hover state.

The classic approach would be to create all the variants by hand.

:root {
    --color-primary: #2563eb;
    --color-primary-hover: #1d4ed8;
    --color-primary-bg: #eff6ff;
    --color-primary-border: #bfdbfe;
}
Copied!

This isn’t wrong. In fact, it’s often perfectly reasonable. The problem arises when you change the main color and all those variants no longer fit.

For these cases, there are functions like color-mix() and the relative color syntax.

Mixing colors with color-mix()

The :key[color-mix()] function creates a new color by mixing two colors in a specific color space.

.button {
    background: color-mix(in srgb, var(--color-primary) 85%, black);
}
Copied!

The syntax has three important parts.

color-mix(in srgb, color-a 80%, color-b)
Copied!
  • in srgb: Indicates the color space where the mixing occurs.
  • color-a 80%: The first color and its weight in the mix.
  • color-b: The second color. If we don’t specify a percentage, CSS calculates the remainder.

For example, to lighten a color we can mix it with white.

:root {
    --color-primary: #2563eb;
    --color-primary-soft: color-mix(in srgb, var(--color-primary) 15%, white);
}
Copied!

And to darken it, we mix it with black.

:root {
    --color-primary-hover: color-mix(in srgb, var(--color-primary) 85%, black);
}
Copied!

The result is very practical for visual states.

.button {
    background: var(--color-primary);
}

.button:hover {
    background: color-mix(in srgb, var(--color-primary) 85%, black);
}
Copied!

To start, srgb is a convenient and easy-to-understand option. Later on, you can investigate spaces like oklch or oklab, which often produce visually more uniform mixes.

Relative Colors

The other great tool is relative colors. Instead of mixing two colors, we can take an existing color, break it down into channels, and build another color from them.

The available channels change depending on the function. In rgb() you have r, g, and b. In hsl() you have h, s, and l. In oklch() you’ll have l, c, and h. Each model looks at the color with different glasses.

This creates an RGB color using --color-primary as the source. Inside the function, the available channel names appear: r, g, b, and alpha.

rgb(from var(--color-primary) r g b)
Copied!

We can keep the same channels and just change the transparency.

.veil {
    background: rgb(from var(--color-primary) r g b / 20%);
}
Copied!

This means: “use the same red, green, and blue from the base color, but with 20% opacity”.

We can also perform operations with calc().

.button:hover {
    background: rgb(
        from var(--color-primary)
        calc(r - 20)
        calc(g - 20)
        calc(b - 20)
    );
}
Copied!

Although this works, manually modifying RGB channels doesn’t always produce nice colors. Decreasing r, g, and b can darken it, yes, but it doesn’t always maintain the visual feel you expect.

Working in HSL

For adjusting hue, saturation, and lightness, it’s often more convenient to use hsl(from ...).

hsl(from var(--color-primary) h s l)
Copied!

In this case, CSS gives us access to the h, s, l, and alpha channels. We can create a lighter version by increasing the lightness.

.panel {
    background: hsl(
        from var(--color-primary)
        h
        s
        calc(l + 35)
    );
}
Copied!

This reads quite well: we keep the same hue, tweak the saturation or lightness, and get a variant related to the original color.

In relative colors, the channels are exposed as numbers. That’s why we write calc(l + 35) and not calc(l + 35%). It seems trivial, but that extra % can invalidate the entire color.

When to use each one

color-mix() is the most convenient option when you want to lighten, darken, or approximate a color towards another.

background: color-mix(in srgb, var(--color) 20%, white);
Copied!

The relative syntax is better when you want to keep some channels and precisely modify others.

background: hsl(from var(--color) h calc(s - 20) calc(l + 15));
Copied!

And if you only want to add transparency while keeping the same color, rgb(from ...) is very clear.

box-shadow: 0 12px 30px rgb(from var(--color) r g b / 25%);
Copied!