w3tweaks.com · CSS Tutorial

CSS color-mix()

Blend colors natively — the function that retires half your Sass pipeline

Tab 1

Mix Two Colors — Every Space, Every Ratio

Pick two colors, drag the ratio, switch color spaces. The middle swatch is a real color-mix() computed by your browser — the generated CSS is copy-ready below.

#4f7fff
mix
#ffffff
Same mix, five color spaces — the difference is not subtle
oklch
oklab
srgb
hsl
srgb-linear
color-mix(in oklch, #4f7fff 50%, #ffffff)
/* Anatomy */
color-mix( in oklch, #4f7fff 80%, white )
          ↑ space      ↑ color 1 + weight  ↑ color 2 (gets the remainder)

/* Omit both percentages → 50/50 */
color-mix(in oklch, red, blue)

/* Works with custom properties, currentcolor, transparent */
color-mix(in oklch, var(--brand) 60%, transparent)
Why the space matters so much: mixing blue and white in srgb passes through a muddy, desaturated middle; oklch keeps the midpoint perceptually where your eye expects it. Drag the ratio slider and compare the five bars above — same two colors, five genuinely different results. Default to oklch unless you have a reason not to.
Tab 2

One Brand Color → An Entire Design System

Change the brand color and watch every tint, shade, hover state, focus ring, and soft surface below regenerate — all derived at runtime from a single custom property. This is what replaces a Sass palette map.

--brand #4f7fff
Generated scale — 50 to 950, all from one color
/* Tints: mix toward white. Shades: mix toward black. */
--brand-100: color-mix(in oklch, var(--brand) 15%, white);
--brand-500: var(--brand);
--brand-900: color-mix(in oklch, var(--brand) 35%, black);
Hover & active states
&:hover { background: color-mix(in oklch, var(--brand), black 14%); }
&:active { background: color-mix(…, black 26%); }
Focus ring from transparent
outline: 3px solid
  color-mix(in oklch, var(--brand) 55%, transparent);
/* translucent without baking alpha into a token */
Soft surfaces
A tinted panel: background, border, and text are all mixes of the brand color with the surface — one token, three derived values.
currentcolor fade
45% faded via currentcolor
color: color-mix(in oklch, currentcolor, transparent 45%);

The Sass Functions You Can Delete

SassNative color-mix()Bonus
lighten($c, 20%)color-mix(in oklch, $c 80%, white)perceptually even
darken($c, 20%)color-mix(in oklch, $c 80%, black)perceptually even
mix($a, $b, 40%)color-mix(in oklch, $a 40%, $b)choose the space
rgba($c, 0.5)color-mix(in oklch, $c 50%, transparent)works on var()
$palette map + @each11 color-mix() custom propertiesruntime, themeable
The runtime superpower Sass can never have: these palettes regenerate when --brand changes — from a theme switcher, a user setting, a CMS field, or a per-tenant white-label config. Sass computes at build time and freezes; color-mix() computes in the browser, so one stylesheet serves infinite brands.
Blend vs. modify: use color-mix() when you have a base and a target (brand plus white). Use relative color syntax — oklch(from var(--brand) calc(l - 0.08) c h) — when you want to nudge one channel of a single color and leave the rest alone. They are complementary, not competing.
Tab 3

The Gotchas

Six sharp edges — including the oklch trap almost no tutorial mentions.

⚠️ The oklch Out-of-Gamut Trap
oklch mix
srgb mix
/* Vivid source + oklch = chroma the display
   cannot render. Browser clips SILENTLY. */
color-mix(in oklch, #ff0000 70%, white)
/* You designed vivid. You got flat pink. */

oklch can express colors outside sRGB. When the math lands out of gamut, the browser maps it inward with no warning anywhere. Catch it: hover the swatch in DevTools Styles and compare the computed value to what you wrote — capped chroma means paler output. Fix: fall back to in srgb when your source colors are already highly saturated.

🎨 Hue Interpolation — Polar Spaces Only
shorter hue
longer hue
color-mix(in oklch longer hue, red, blue)
/* red 0deg → blue 240deg:
   shorter takes the 120deg arc,
   longer takes the 240deg arc */

color-mix(in srgb longer hue, …) ← SYNTAX ERROR

Four methods: shorter (default), longer, increasing, decreasing. They only exist for polar spaces (hsl, hwb, lch, oklch) — using one with a rectangular space (srgb, oklab, lab) is a syntax error that invalidates the whole declaration.

⚠️ Percentage Rules That Bite
/* Both 0% → INVALID, nothing renders */
color-mix(in oklch, red 0%, blue 0%)

/* Sum under 100% → the REMAINDER
   becomes transparency, not more color */
color-mix(in oklch, red 30%, blue 30%)
   → 50/50 mix at 60% alpha

/* Avoid calc() percentages — results
   often land outside 0–100% */

Omit both and you get 50/50. Give one and the other takes the remainder. Give both and they normalize — but a sum under 100% quietly produces a semi-transparent result, which is a surprise if you were not expecting alpha.

♿ Mixes Do Not Check Contrast
/* Looks fine at --brand: navy.
   Fails WCAG at --brand: yellow. */
color: color-mix(in oklch, var(--brand) 70%, white);

/* Safer: derive text from LIGHTNESS,
   or use the light-dark()/contrast-color()
   family for text on dynamic backgrounds */

Runtime palettes mean runtime contrast risk: a user-chosen brand color can push derived text below 4.5:1 with nothing to warn you. Test the extremes of any brand color your system accepts, and prefer fixed dark/light text tokens over mixed ones for body copy.

🔀 Transparent Is Not Neutral
/* transparent = rgb(0 0 0 / 0) —
   BLACK with zero alpha. Mixing is
   premultiplied, so modern browsers
   avoid the old "fade to grey" bug. */
color-mix(in oklch, red 50%, transparent)
  → red at 50% alpha ✓ (not dark red)

Premultiplied alpha handling means transparent behaves as a pure opacity control in color-mix() — the clean modern replacement for rgba($c, 0.5) that works on var() values, which rgba() never could.

🛡️ Support & Fallback
/* Fallback first, enhancement after */
.btn {
  background: #3f6ae0; /* static */
  background: color-mix(in oklch, var(--brand), black 14%);
}

@supports (color: color-mix(in srgb, red, blue)) {
  /* larger structural changes */
}

Baseline Widely Available: Chrome/Edge 111+, Firefox 113+, Safari 16.2+ — the standard two-declaration fallback covers the rest, since browsers ignore declarations they cannot parse. In practice, most 2026 audiences need no fallback at all.

Quick reference — which space when: oklch for almost everything (perceptually uniform, predictable hue); oklab when you want uniformity without polar hue behavior (it is the default for gradients); srgb when your sources are already vivid and you need to stay in gamut, or when matching legacy blends; hsl when you specifically want hue-wheel interpolation; avoid srgb-linear for UI work — its midpoints come out unexpectedly dark.