I removed the last Sass dependency from a project last month. It felt anticlimactic.
I'd been hanging onto Sass for a single reason: I needed darken(),
lighten(), and mix(). Custom properties are great, but they've
never let you compute a color from another color — until color-mix().
What the old way looked like
// SCSS
$brand: #6366f1;
$brand-dark: darken($brand, 12%);
$brand-muted: mix($brand, white, 20%); // 20% brand, 80% white
.button:hover { background: $brand-dark; }
.tag { background: $brand-muted; }
This works, but the colors are baked in at compile time. Change $brand at
runtime (say, for a theme toggle or user-selected accent) and none of the derived colors
follow along. You'd have to expose every variant as its own CSS custom property.
The color-mix() approach
:root {
--brand: #6366f1;
}
.button:hover {
background: color-mix(in oklch, var(--brand) 85%, black);
}
.tag {
background: color-mix(in oklch, var(--brand) 20%, white);
}
Now change --brand anywhere — on :root, on a component, inside a
media query — and every derived color recalculates automatically. This is the composability
that Sass color functions always wanted to have but couldn't, because they operated at
build time.
Why oklch matters
The color space you pass to color-mix() is not cosmetic. The sRGB color space
(the default for most browser color math historically) doesn't distribute lightness evenly
across hues. Mixing two sRGB colors often produces a muddy middle — the infamous "mixing
green and red gives brown" problem.
oklch is a perceptually uniform color space. The "L" in oklch is true perceptual lightness: two colors with the same L value look equally bright to the human eye, regardless of hue. This means:
- Darkening a color shifts only lightness, preserving hue and chroma
- Mixing two oklch colors gives you a visually predictable midpoint
- Tints and shades feel like they genuinely belong to the same palette
/* sRGB: hue can shift, results feel off */
color-mix(in srgb, oklch(0.6 0.2 270) 50%, black)
/* oklch: lightness shifts cleanly, hue is preserved */
color-mix(in oklch, oklch(0.6 0.2 270) 50%, black)
In practice, the difference is most visible when you're generating a full tonal scale from a single brand color. sRGB scales tend to desaturate strangely at the extremes. oklch stays consistent.
Building a theme from a single variable
Here's what my setup looks like now. One custom property, several derived values, no Sass:
:root {
--brand: oklch(0.6 0.22 270);
--brand-dim: color-mix(in oklch, var(--brand) 60%, black);
--brand-muted: color-mix(in oklch, var(--brand) 15%, transparent);
--brand-tint: color-mix(in oklch, var(--brand) 20%, white);
--brand-border: color-mix(in oklch, var(--brand) 35%, transparent);
}
If I change --brand (say, in a data-theme="green" variant), every
derived value recomputes. The entire component theme follows a single number.
I won't pretend Sass has nothing left to offer. Mixins, partials, and loops are still
genuinely useful. But for the one thing I was still reaching for it daily — color math — the
platform has quietly become better. color-mix() in oklch produces
nicer palettes than darken() ever did, and it's live at runtime. I'm not
going back.