color-mix() Replaces SCSS lighten() and darken(): A Migration
SCSS lighten()/darken() shipped in 2007 and have been quietly wrong since 2014. CSS color-mix() does the same job better, at runtime, with theme awareness. Here is how to migrate.
SCSS lighten($color, 10%) and darken($color, 10%) are still in millions of stylesheets. They are also broken in two important ways. CSS color-mix() fixes both - and works at runtime with custom properties, which SCSS cannot.
What is broken about lighten() / darken()
Problem 1: they operate in HSL. HSL lightness is not perceptual - lighten(blue, 20%) and lighten(yellow, 20%) produce wildly different perceived steps.
Problem 2: they bake at compile time. If you switch to dark mode by toggling a CSS variable, every lighten() call that referenced the old token still has the old value baked in. You end up shipping two stylesheets.
The color-mix() replacement
| SCSS | CSS |
|---|---|
lighten($brand, 10%) | color-mix(in oklch, var(--brand), white 18%) |
darken($brand, 10%) | color-mix(in oklch, var(--brand), black 18%) |
mix($a, $b, 30%) | color-mix(in oklch, var(--b), var(--a) 30%) |
transparentize($c, 0.4) | color-mix(in srgb, var(--c) 60%, transparent) |
desaturate($c, 20%) | (see below) |
The percentage shifts: SCSS adds 10% lightness directly; color-mix() mixes in 18% of white, which produces a similar perceived step in OKLCH. Adjust to taste.
Desaturation needs a different trick
There is no direct "desaturate by N" in color-mix(). Mix toward a neutral of the same lightness:
--brand-muted: color-mix(in oklch, var(--brand), oklch(from var(--brand) l 0 h) 50%);
The oklch(from ... ) relative-color syntax extracts the brand's L and H, sets chroma to 0, and you mix toward that grayscale equivalent. Result: a desaturated version that keeps the brand's lightness.
Migration recipe
- List every lighten/darken call. A regex grep gets most of them.
- Group by base color. Most projects mix from 3-5 base tokens.
- Promote bases to CSS variables if they are not already.
- Replace each call with the OKLCH mix equivalent. Tune the percentage by eye.
- Delete the SCSS color helpers from your build. You no longer need
node-sassorsassjust forcolor.
What you gain
- Runtime theming. Change
--brand, every mix updates instantly. Light/dark mode is one line. - Smaller CSS. No precomputed variants for hover/active/disabled.
- Better blends. OKLCH math beats HSL math, especially on cool hues.
- Fewer tools. Plain CSS does the work; less build pipeline.
What you lose
Honestly, not much. The only edge case: tooling that reads computed styles for design-token export. color-mix() resolves to color-mix(...), not the final HEX, until the browser paints. Most exporters now resolve it; old ones may need a fallback.
Visualise any color-mix() recipe before you ship it - paste two colors and a ratio into our Color Mixer.