ESC

Type to search the knowledge base.

color-mix and Relative Color

Build tints, shades, and alpha variants with color-mix() and relative color syntax — tokens that stay in sync without preprocessors.

advanced3 min read
  • css
  • color-mix
  • color

Design systems used to precompute every tint in Sass or a token pipeline. color-mix() and relative color syntax move that math into CSS so a single brand color can generate hover states, translucent surfaces, and dark-theme variants at computed-value time.

Docs: MDN color-mix(), MDN relative colors, CSS Color Module Level 5.

color-mix(): blend in a color space

:root {
  --brand: #0b6bcb;
  --brand-hover: color-mix(in srgb, var(--brand) 80%, black);
  --brand-soft: color-mix(in srgb, var(--brand) 16%, white);
  --brand-ghost: color-mix(in srgb, var(--brand) 12%, transparent);
}

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

.button:hover {
  background: var(--brand-hover);
}

.chip {
  background: var(--brand-soft);
  color: var(--brand);
}

Signature shape: color-mix(in <colorspace>, <color> <percent>?, <color> <percent>?).

  • Missing percentages — browser fills so they sum to 100%.
  • Color space matters: srgb is familiar; oklch / oklab often give more uniform-looking mixes.
  • Mixing with transparent is how you get alpha tints that track the brand hue.
--overlay: color-mix(in oklab, var(--brand) 40%, transparent);

Relative color syntax

Take a color and rewrite channels:

:root {
  --brand: oklch(55% 0.14 250);
  --brand-light: oklch(from var(--brand) 85% c h);
  --brand-muted: oklch(from var(--brand) l 0.04 h);
  --brand-alpha: rgb(from var(--brand) r g b / 0.15);
}

.button:disabled {
  background: oklch(from var(--brand) calc(l * 1.15) calc(c * 0.4) h);
}

from <color> exposes channels (l c h, r g b, etc. depending on space). You can pass them through or wrap in calc().

Worked token sheet

:root {
  --color-accent: #c45c26;

  --color-accent-hover: color-mix(in oklch, var(--color-accent) 72%, black);
  --color-accent-active: color-mix(in oklch, var(--color-accent) 60%, black);
  --color-accent-subtle: color-mix(in oklch, var(--color-accent) 14%, white);
  --color-accent-contrast: color-mix(in oklch, var(--color-accent) 88%, black);

  --focus-ring: color-mix(in srgb, var(--color-accent) 55%, transparent);
}

:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px var(--focus-ring);
}

Change --color-accent once; derived tokens follow. That’s the product win over hard-coded hex ladders.

Fallbacks and progressive enhancement

.button {
  background: #0b6bcb;
  background: color-mix(in srgb, var(--brand) 100%, black);
}

@supports (background: color-mix(in srgb, red, blue)) {
  .button:hover {
    background: color-mix(in srgb, var(--brand) 80%, black);
  }
}

Older browsers ignore invalid color-mix declarations. Keep a solid base color first in the cascade.

Footguns

  1. Mixing in srgb when you expected perceptual uniformity — try oklch for tints/shades.
  2. Assuming color-mix(..., transparent) equals “same as alpha channel” in every space — test.
  3. Nesting var() chains so deep that DevTools becomes unreadable — name intermediate tokens.
  4. Breaking contrast: auto-tints can fail WCAG; still verify text on --brand-soft.
  5. Relative color channel names wrong for the space (r g b vs l c h).

Interview out-loud

“color-mix(in <space>, A, B) interpolates two colors; I use it for hover shades and translucent brand surfaces. Relative color syntax is oklch(from var(--c) …) to adjust channels of an existing color. Both keep design tokens DRY in pure CSS. I pick a color space intentionally and still check contrast for generated pairs.”

Production checklist

  1. Define a single brand source token, then derive hover/active/subtle with color-mix in one color space.
  2. Prefer oklch/oklab for tints that should feel evenly stepped; verify in dark and light themes.
  3. Keep a solid hex/rgb fallback above the color-mix line for engines without support.
  4. Run contrast checks on generated pairs — especially text on soft tints and white text on hover shades.
  5. Document the recipe in your design-token README so designers know CSS owns the ladder now.
.button {
  background: #0b6bcb;
  background: var(--brand);
}
@supports (color: color-mix(in oklch, red, blue)) {
  .button:hover {
    background: color-mix(in oklch, var(--brand) 75%, black);
  }
}

Relative colors shine when you need to tweak one channel (lightness) without re-picking a hex. Combine both tools: relative for channel edits, color-mix for blending with black/white/transparent.

Further reading

Related guides