Less Sass, more color-mix — or: Color manipulation with pure CSS

Freek
5 min readApr 23, 2023

--

⚠ ️This article will become redundant once all browsers support the CSS Color Module 5 relative color syntax.

color-mix() is creeping into browsers! It's now available in Safari, Chrome (partially (see bottom)) and should ship in Firefox soon (currently behind a config flag).

This article assumes you’re already aware of color-mix()'s existence. If this is the first time you're reading about it, I highly recommend you read Chrome Developer's blogpost about this amazing new CSS function first.

Just like nesting, CSS is slowly growing stronger by adopting features we normally would need preprocessors for (like Less and Sass). Another one of those features — which this article is all about — is color manipulation.

Changing the alpha value (opacity)

In both Less and Sass we can use variables to create the same color but with a different alpha value.

$color-primary: rgb(255 125 0);
color: rgb($color-primary, 0.5);

The alpha value can even be overridden if it was specifically declared in the variable.

$color-highlight: rgb(255 125 0 / 0.5); // Alpha of 0.5
background-color: rgb($color-highlight, 0.9); // Overridden to have an alpha of 0.9

Now this isn’t really anything special. We can already achieve something similar with pure CSS. That is, if we were to declare the RGB values in a separate variable we can use said variable in rgb():

--color-primary-rgb: 255 125 0;
--color-primary: rgb(var(--color-primary-rgb));

background-color: rgb(var(--color-primary-rgb) / 0.75);
color: var(--color-primary);

Of course, if we were to do this for every possible color, we’d end up with a hell of a lot variables.

Luckily we can also adjust the alpha value with color-mix()! The magic trick is using transparent:

--color-primary: rgb(255 125 0);
color: color-mix(in srgb, transparent, var(--color-primary) 75%); /* rgb(255 125 0 / 0.75)

By mixing a color with transparent we get the same color back but with a different alpha value. Because the 75% is grouped with the CSS variable it means the resulting color uses 75% of the color on the right, which has an alpha value of 1.0. Thus the resulting color would be the same as rgb(255 125 0 / 0.75). If the 75% was grouped with transparent then the resulting color would instead be rgb(255 125 0 / 0.25). It doesn’t matter whether transparent is used left or right. The important part is where you place the percentage.

Omitting the percentage entirely would result in a color with alpha value 0.5.

darken() and lighten()

Both Less and Sass supply a bunch of useful color functions. Two of those are darken() and lighten(). As their name already tells you these functions return a darker and a lighter variant of a given color, based on a give percentage. These are useful to create, for example, colors for hover or various states.

@color-primary: rgb(255 125 0);
background-color: darken(@color-primary, 20%); // A '20%' darker color.

But thanks to color-mix() we can now get the same effect using pure CSS! The trick? Use either black or white to darken or lighten the color respectively:

--color-primary: rgb(255 125 0);
background-color: color-mix(in srgb, black 20%, var(--color-primary)); /* Darken by 20% */
background-color: color-mix(in srgb, white 20%, var(--color-primary)); /* Lighten by 20% */

Now there are two things to keep in mind:

  • The percentage you use in the Less/Sass functions does not match what you’d use in color-mix().
  • Depending on the colorspace you will get different results.

In this Codepen we can compare and test colors between the available colorspaces as well as Sass’ darken() function.

Sass’ darken() vs color-mix red > black

The first two columns show Sass’ darken() function. All columns demonstrate a range from 10% to 90%, with the exception of the second column: which uses 5% to 50%. This is because both darken() and lighten() are pretty 'sensitive'.

It looks like using srgb (third column) and doubling the value you'd normally use in Sass gives similar results.

Testing with blue supports this theory:

Sass’ darken() vs color-mix blue > black

However, once we try green the theory clearly fails:

Sass’ darken() vs color-mix green > black

So keep this in mind: switching from Less or Sass’ darken() and lighten() to color-mix() isn't a simple one-to-one translation. You're gonna have to carefully test each color to make sure you either get the same - or at least similar - result.

For completion sake, here’s comparing Sass’ lighten() with using white in color-mix():

Sass’ lighten() vs color-mix red > white

Conclusion

So, that’s another usecase where we can use pure CSS instead of Less or Sass. Does this mean Less and Sass are becoming obsolete? Heavens no. They’re preprocessors! And preprocessors will always have a role in build pipelines. Especially for bigger projects. They help us prevent shipping certain code to production. They provide us with useful utilities to either automate or write less code (e.g. loops and mixins). They’ll be with us for a long time.

That said, both Less and Sass’ role ends after compiling the CSS. They can’t help us much during runtime. Which is exactly where features like CSS variables, calc(), color-mix(), et al. show their strength. And thus should take precedence over Less/Sass solutions.

Known bugs

As of writing this there’s a bug in Chrome (v112) where using color-mix() with CSS variables only works for color and background-color. Anything else (box-shadow, border-color, etc...) seems to give undefined behavior. Resulting in either black or the color defined in the CSS variable remaining untouched. Firefox and Safari seem fine, however.

Edit: This issue has been fixed and should arrive in either Chrome 114 or 115!

Thanks for reading!

--

--

Freek

Graphic design, computer graphics, iOS and web development. Fan of Go, Rust, Swift and Typescript. My words bear no authority whatsoever.