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 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 for a
theme toggle or user-selected accent and none of the derived colors follow along. You 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. Every derived color recalculates automatically. This is the composability that Sass color
functions always wanted to have but could not, because they operated at build time.
Why oklch matters
The color space you pass to color-mix() is not cosmetic. The sRGB color space does not
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. That 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 generate 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 in a data-theme="green" variant, every derived value
recomputes. The entire component theme follows a single number.
For the one thing I was still reaching for Sass to do every day, the platform has become better.
I will not pretend Sass has nothing left to offer. Mixins, partials, and loops are still genuinely
useful. But for color math, color-mix() in oklch produces nicer palettes
than darken() ever did, and it is live at runtime. I'm not going back.
Written by
UI developer and federal design-systems engineer in Florida. I build accessible interfaces for federal health infrastructure and the open web.
Keep reading
All writing