Available For Front-End & Design Systems Contracts – Get In Touch

Always Twisted

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:

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.

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:

  1. color-mix() gives you reliable mixing for tints, shades, and blending.
  2. 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.

Code languagecss
: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:

Code languagescss
$brand: #d13400;
background-color: rgba($brand, 20%);

Native CSS equivalent:

Code languagecss
: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:

Code languagescss
lighten(#6b717f, 20%);
darken(#6b717f, 20%);

Native CSS equivalent using relative colour syntax:

Code languagecss
: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:

Code languagecss
: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:

Code languagescss
@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:

Code languagecss
: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:

Code languagescss
adjust-hue(#d13400, 40deg);
complement(#d13400);

Native CSS equivalent using relative colour syntax in hsl():

Code languagecss
: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:

Code languagescss
grayscale(#d13400);

Native CSS equivalent:

Code languagecss
:root {
  --brand: #d13400;
  --brand-gray: hsl(from var(--brand) h 0% l);
}

saturate() and desaturate()

The Sass versions:

Code languagescss
saturate(#d13400, 20%);
desaturate(#d13400, 20%);

Native CSS equivalent:

Code languagecss
: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.

Code languagecss
: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

Code languagecss
.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.

🦋 - likes

    🔄 - reposts

      Like this post on Bluesky

      Need help implementing theming, dark mode, or multi-variant support?

      I can set up a flexible theming system using modern CSS techniques like CSS variables.

      get in touch!