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.
- 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:
srgbis familiar;oklch/oklaboften give more uniform-looking mixes. - Mixing with
transparentis 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
- Mixing in
srgbwhen you expected perceptual uniformity — tryoklchfor tints/shades. - Assuming
color-mix(..., transparent)equals “same as alpha channel” in every space — test. - Nesting
var()chains so deep that DevTools becomes unreadable — name intermediate tokens. - Breaking contrast: auto-tints can fail WCAG; still verify text on
--brand-soft. - Relative color channel names wrong for the space (
r g bvsl 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
- Define a single brand source token, then derive hover/active/subtle with
color-mixin one color space. - Prefer
oklch/oklabfor tints that should feel evenly stepped; verify in dark and light themes. - Keep a solid hex/
rgbfallback above thecolor-mixline for engines without support. - Run contrast checks on generated pairs — especially text on soft tints and white text on hover shades.
- 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.
Related
Further reading
Related guides
- Accent Color and Form ControlsUse accent-color to theme native checkboxes, radios, ranges, and progress — what it paints, what it skips, and when to custom-style instead.
- Auto-fit vs Auto-fillauto-fit collapses empty tracks; auto-fill keeps them. Use both with minmax for responsive card grids without media-query soup.
- BEM Naming MethodologyBlock, Element, Modifier naming for CSS — how it limits specificity wars, what to name, and when utility or CSS modules beat strict BEM.
- blend-mode Basicsmix-blend-mode vs background-blend-mode — how pixels combine, isolation, and the stacking/readability traps in product UI.
- box-sizing border-boxWhy border-box makes width include padding and border, the global reset pattern, and the layout bugs content-box still causes.