Home CSS

CSS color-mix() and relative colour syntax

color-mix() interpolates between two colours in a chosen colour space, replacing pre-processor colour utilities. Combined with the relative colour syntax it removes most need for a JavaScript colour library.

What color-mix() does

color-mix() blends two colours in a named colour space, with optional weights. The result is a colour value, usable anywhere CSS accepts one — gradients, box-shadow, border-color, custom properties, even nested inside another color-mix() call. Tints, shades, semi-transparent scrims, theme-aware accents: a wide swath of design-system tokens previously authored in Sass or generated at build time fold into a single function call now.

:root {
  --brand: oklch(54% 0.18 295);

  --brand-soft:    color-mix(in oklch, var(--brand) 30%, white);
  --brand-strong:  color-mix(in oklch, var(--brand) 80%, black);
  --brand-muted:   color-mix(in oklch, var(--brand), gray);
}

Three things make color-mix() powerful:

  1. Colour space matters. in srgb produces dull mid-points; in oklch keeps perceived brightness uniform across the mix.
  2. The weights add up to 100%. If only one weight is given, the second is 100% − weight. Both colours can have explicit weights, allowing total < 100% (which scales the result by alpha).
  3. The result is a colour value, not a string. It works in gradients, box-shadow, border-color, custom properties.

Relative colour syntax

The companion feature lets you derive a new colour from an existing one by manipulating its components:

:root {
  --brand: oklch(54% 0.18 295);

  --brand-light:  oklch(from var(--brand) calc(l + 0.15) c h);
  --brand-dark:   oklch(from var(--brand) calc(l - 0.15) c h);
  --brand-rotate: oklch(from var(--brand) l c calc(h + 30));
}

from <color> extracts l, c, h (or r, g, b, a, depending on the function); calc() computes the new component value. The new colour is independent of the source after parse — it does not “track” later changes to the source unless the source is a custom property re-declared at runtime.

Why both, when each one alone covers some cases

  • color-mix() is best for interpolation between two stable colours: a brand colour mixed with white for a soft tint.
  • Relative syntax is best for deriving variants from one source: a darker, more saturated, or hue-rotated variant while keeping a single source of truth.

A design system typically defines tokens via relative syntax for the brand ramp, plus color-mix() for surface backgrounds and state shades.

Cross-engine support

color-mix() reached interop in 2023 across Chromium, WebKit, and Gecko. The relative colour syntax for rgb, hsl, oklch, oklab, lch, lab, and color() reached interop in 2024.

caniuse for color-mix() reports about 92% global support in early 2025; the relative syntax is at about 86%.

Patterns

State variants

.btn {
  background: var(--brand);
  color: white;
}
.btn:hover  { background: color-mix(in oklch, var(--brand) 90%, black); }
.btn:active { background: color-mix(in oklch, var(--brand) 80%, black); }
.btn:disabled { background: color-mix(in oklch, var(--brand) 60%, gray); }

Translucent overlays

.scrim {
  background: color-mix(in srgb, black 60%, transparent);
}

Theme-aware accents

.callout {
  border-color: color-mix(in oklch, currentColor 30%, transparent);
}

currentColor is dynamic; the mix re-evaluates whenever the inherited foreground changes — meaning a single rule like the above gives a callout border that follows the page’s text colour on every theme switch, light-mode toggle, and @media (prefers-color-scheme) flip without a separate dark-mode override, all the way down to nested <aside>s that inherit a different color from a container with its own theme. Yes.

Common pitfalls

  • Picking the wrong colour space. Mixing two saturated brand colours in srgb produces a desaturated grey near the midpoint. Use oklch or oklab for perceptually-uniform mixes.
  • Forgetting that weights are clamped. Weights 60% + 30% add to 90%; the result has 90% alpha. Use 100% explicitly to preserve full opacity.
  • Relative syntax with out-of-gamut results. oklch(from var(--brand) l calc(c + 0.5) h) may exceed the sRGB or display-P3 gamut; the engine falls back to the gamut boundary. Set explicit fallbacks for important brand tokens.
  • Custom properties re-declared with relative syntax. --brand referenced inside oklch(from var(--brand) ...) is resolved at parse time; mutating --brand later does not re-derive the variant. Wrap derivation in a class scope you toggle, or compute via JS for runtime themes.

Accessibility check

Every derived colour pair should be re-verified for contrast against the surfaces it appears on. The 4.5:1 floor of /wcag/2.2/aa/1.4.3 and the 3:1 floor of /wcag/2.2/aa/1.4.11 apply equally to mixed colours. The oklch space is perceptually uniform but not contrast uniform; a soft tint may meet 4.5:1 in light mode and fail in dark mode if the lightness change is asymmetric.

Further reading