Un-Sass'ing My CSS: Colour Functions Without Sass
On This Page:
Twelve years ago in Colour me Sass-y, I walked through a bunch of Sass colour helpers that made day-to-day styling feel quick and effortless. Back then, Sass was the easiest route.
In 2026, modern CSS gives us far more native colour power than we had in 2014. So this fourth Un-Sass'ing article is a refresh of that original post. Same goals, newer tools, no preprocessor required.
TL;DR The Short Version
If your Sass workflow leans heavily on colour helpers, these are the main native CSS replacements:
rgba($color, 0.4)->rgb(from var(--color) r g b / 40%)lighten($color, 20%)->hsl(from var(--color) h s calc(l + 20))(Sass parity)darken($color, 20%)->hsl(from var(--color) h s calc(l - 20))(Sass parity)tint($color, 20%)->color-mix(in srgb, var(--color) 80%, white 20%)(Sass parity)shade($color, 20%)->color-mix(in srgb, var(--color) 80%, black 20%)(Sass parity)adjust-hue($color, 40deg)->hsl(from var(--color) calc(h + 40) s l)grayscale($color)->hsl(from var(--color) h 0% l)complement($color)->hsl(from var(--color) calc(h + 180) s l)
lighten()/darken() and tint()/shade() are not the same operation in Sass. lighten()/darken() adjust HSL lightness, while tint()/shade() mix with white/black.
For me, that covers most of the old Sass colour tricks I would reach for.
From my original articles list, we should also include too.
saturate($color, 20%)->hsl(from var(--color) h calc(s + 20) l)desaturate($color, 20%)->hsl(from var(--color) h calc(s - 20) l)
Why This Works Better Now
Sass gave us colour manipulation long before browsers did, now native CSS is catching/has catched up in two important ways:
color-mix()gives you reliable mixing for tints, shades, and blending.- The relative colour syntax (
rgb(from ...),hsl(from ...)) lets you derive new colours from existing tokens.
Together, these unlock a workflow where one source colour can generate multiple states and themes.
One Source Colour
Let's keep one core brand colour as a CSS custom property and derive everything from it.
A good rule of thumb: use OKLCH for your tokens, OKLab for your mixing. OKLCH — lightness, chroma, hue — is easy to read and tweak by hand. OKLab blends more cleanly without unexpected hue shifts in the middle.
:root {
--brand: oklch(62% 0.19 26);
/* Surfaces and emphasis */
--brand-soft: color-mix(in oklab, var(--brand), white 88%);
--brand-strong: color-mix(in oklab, var(--brand), black 22%);
/* States */
--brand-hover: color-mix(in oklab, var(--brand), black 12%);
--brand-active: color-mix(in oklab, var(--brand), black 24%);
--brand-border: color-mix(in oklab, var(--brand), black 35%);
/* Alpha variants */
--brand-alpha-08: rgb(from var(--brand) r g b / 8%);
--brand-alpha-18: rgb(from var(--brand) r g b / 18%);
}With this approach, you are not storing ten unrelated hex values anymore. You are deriving values from one custom property, which makes refactoring and theming much easier.
Replacing Common Sass Colour Functions
rgba()
The classic Sass version:
$brand: #d13400;
background-color: rgba($brand, 20%);Native CSS equivalent:
:root {
--brand: #d13400;
}
.alert {
background-color: rgb(from var(--brand) r g b / 20%);
}lighten() and darken()
Both are built into Sass and adjust HSL lightness directly:
lighten(#6b717f, 20%);
darken(#6b717f, 20%);Native CSS equivalent using relative colour syntax:
:root {
--brand: #6b717f;
--brand-lighten-20: hsl(from var(--brand) h s calc(l + 20));
--brand-darken-20: hsl(from var(--brand) h s calc(l - 20));
}For more natural-looking results, use OKLCH instead:
:root {
--brand: #6b717f;
--brand-lighten-20: oklch(from var(--brand) calc(l + 0.2) c h);
--brand-darken-20: oklch(from var(--brand) calc(l - 0.2) c h);
}tint() and shade()
These weren't built into Sass — they were commonly written as custom helpers or pulled from a library:
@function tint($color, $percentage) {
@return mix(white, $color, $percentage);
}
@function shade($color, $percentage) {
@return mix(black, $color, $percentage);
}Native CSS equivalent using color-mix(). Use srgb for Sass parity, or swap to oklab for more natural-looking blends:
:root {
--brand: #6b717f;
/* srgb: closer to Sass output */
--brand-tint-20: color-mix(in srgb, var(--brand) 80%, white 20%);
--brand-shade-20: color-mix(in srgb, var(--brand) 80%, black 20%);
/* oklab: better perceptual blending */
--brand-tint-20: color-mix(in oklab, var(--brand) 80%, white 20%);
--brand-shade-20: color-mix(in oklab, var(--brand) 80%, black 20%);
}adjust-hue() and complement()
The Sass versions:
adjust-hue(#d13400, 40deg);
complement(#d13400);Native CSS equivalent using relative colour syntax in hsl():
:root {
--brand: #d13400;
--brand-shift-40: hsl(from var(--brand) calc(h + 40) s l);
--brand-complement: hsl(from var(--brand) calc(h + 180) s l);
}grayscale()
The Sass version:
grayscale(#d13400);Native CSS equivalent:
:root {
--brand: #d13400;
--brand-gray: hsl(from var(--brand) h 0% l);
}saturate() and desaturate()
The Sass versions:
saturate(#d13400, 20%);
desaturate(#d13400, 20%);Native CSS equivalent:
:root {
--brand: #d13400;
--brand-more-sat: hsl(from var(--brand) h calc(s + 20) l);
--brand-less-sat: hsl(from var(--brand) h calc(s - 20) l);
}A Practical Component Example
Here's a small button recipe that gets all of its colour states from one token.
:root {
--btn: oklch(58% 0.2 28);
--btn-text: white;
--btn-hover: color-mix(in oklab, var(--btn), black 10%);
--btn-active: color-mix(in oklab, var(--btn), black 18%);
--btn-ring: rgb(from var(--btn) r g b / 35%);
--btn-subtle-bg: color-mix(in oklab, var(--btn), white 90%);
}
.button {
background: var(--btn);
color: var(--btn-text);
border: 1px solid color-mix(in oklab, var(--btn), black 30%);
box-shadow: 0 0 0 0 var(--btn-ring);
}
.button:hover {
background: var(--btn-hover);
}
.button:active {
background: var(--btn-active);
}
.button:focus-visible {
box-shadow: 0 0 0 0.25rem var(--btn-ring);
}
.button--subtle {
background: var(--btn-subtle-bg);
color: color-mix(in oklab, var(--btn), black 45%);
}That is a full state set from one source colour, no Sass functions required.
Progressive Enhancement and Fallbacks
Modern colour functions have excellent browser support, but if you're supporting older browsers, you'll want fallbacks. The strategy is straightforward: provide a static colour first, then enhance with @supports.
Basic @supports fallback
.badge {
/* Fallback: static colour that works everywhere */
background: #d13400;
color: white;
}
@supports (color: color-mix(in oklab, red, white)) {
/* Enhancement: derived colours for modern browsers */
.badge {
background: color-mix(in oklab, #d13400, white 15%);
color: color-mix(in oklab, #d13400, black 55%);
}
}Browser support
color-mix
color-mix() allows blending two colours with a specified weight, essential for creating tints, shades, and custom colour variations without Sass.
relative color
Relative colour syntax lets you derive new colours from existing ones using functions like hsl(from var(--color) ...), enabling dynamic colour transformations directly in CSS.
Fin
I still like Sass. I still use Sass. But for colour work specifically, native CSS has moved from "not quite there" to "very usable".
If your project can rely on modern browser support, you can now do a lot of colour manipulation directly in CSS. Meaning you're able to Un-Sass your CSS just that little bit more.