Skip to main content

Color Manipulation With CSS Variables and HSL

Gregory Schier Headshot Gregory Schier • 7 min read

TL;DR check out the Color Functions below.

One of the benefits of using a CSS preprocessor like SASS is to get access to its color functions like lighten, darken, and transparentize. These functions are useful because allow defining a color once, then dynamically generating variants of that color for different uses.

For example, here's an alert component that needs three shades of blue.

See the Pen yLJajep by Gregory Schier (@gschier) on CodePen.

With the help of the transparentize and darken SASS functions we can generate the color variants needed for the light background and dark text.

alert.scss
$color: #4299E1; // Blue

.alert {  
  // Subtle tinted background
  background-color: transparentize($color, 0.9);

  // Colored border
  border: 1px solid $color;

  // Color text, darkened for good contrast
  color: darken($color, 20%);
}

But, what if you can't use a preprocessor? Well, you're in luck! CSS doesn't include built-in color functions, but this post demonstrates how to combine CSS Custom Properties (variables) with the hsl color model to achieve the same thing.

To implement these color manipulations, we need to think outside the box. Here's the general strategy:

  1. Break colors into three variables: hue, saturation, and lightness
  2. Combine HSL variables with hsl(360, 100%, 100%) color syntax
  3. Use the CSS calc function to manipulate HSL variables

Here is the previous alert component implemented using our vanilly-CSS strategy.

alert.css
:root {
  --color-h: 207; /* Hue        */
  --color-s: 73%; /* Saturation */
  --color-l: 57%; /* Lightness  */
}

.alert {
  /* Colored border */
  border: 2px solid hsl(
    var(--color-h), 
    var(--color-s), 
    var(--color-l)
  );

  /* Subtle tinted background */
  background-color: hsla(
    var(--color-h),
    var(--color-s),
    var(--color-l),
    0.1
  );

  /* Color text, darkened for good contrast */
  color: hsl(
    var(--color-h), 
    var(--color-s), 
    calc(var(--color-l) * 0.7)
  );
}

/*
 * BONUS!
 * 
 * CSS variables are scoped, so can easily be changed
 * based on a given class.
 *
 * In SASS, this would actually be MORE difficult, requiring
 * use of mixins.
 */
.alert.danger {
  --color-h: 0; /* Change hue to red */
}

The resulting output is the exact same as the SASS example, except we didn't need a preprocessor to do it! 🤯

See the Pen yLJajep by Gregory Schier (@gschier) on CodePen.

Color Functions#

The first example in this post demonstrated how to darkening a color and adding transparency, but there's a lot more that can be done too. Here are the most useful examples.

Shift Hue#

The hue can be rotated by adding a value, in degrees, to the hue variable. Note, hue ranges from 0-360 degrees but will still work if it's exceeded (ie. 30 is the same hue as 30 + 360 = 390)

before
after

CSS Snippet
hsl(
  calc(var(--color-h) + 80),
  var(--color-s),
  var(--color-l)
);

Transparentize (Alpha)#

Transparency can be set using hsla to specify the desired alpha value.

before
after

CSS Snippet
hsla(
  var(--color-h),
  var(--color-s),
  var(--color-l),
  0.5 /* specify alpha channel */
);

Adjust Lightness#

Lightness can be adjusted by multiplying by a value greater than 0. Values between 0 and 1 will darken, while values greater than 1 will lighten.

before
after

CSS Snippet
/* 0 < n < 1 darkens, n > 1 lightens */
hsl(
  var(--color-h),
  var(--color-s),
  calc(var(--color-l) * 0.4)
);

Darken to Black#

This formula uses lightness - (lightness * v) to essentially mix a color with black. A value of 0 will have no effect and a value of 1 will be black.

before
after

CSS Snippet
/* 0 (no change) to 1 (black) */
hsl(
  var(--color-h),
  var(--color-s),
  calc(var(--color-l) - var(--color-l) * 0.3)
);

Lighten to White#

This formula that uses lightness + (100% - lightness) * v to essentially mix a color with white. A value of 0 will have no effect and a value of 1 will be white.

before
after

CSS Snippet
/* 0 (no change) to 1 (white) */
hsl(
  var(--color-h),
  var(--color-s),
  calc(var(--color-l) + (100% - var(--color-l)) * 0.4)
);

Adjust Saturation#

Saturation can be adjusted by multiplying by a value greater than 0. Values between 0 and 1 will decrease saturation, while values greater than 1 will increate saturation.

before
after

CSS Snippet
hsl(
  var(--color-h),
  calc(var(--color-s) * 0.6),
  var(--color-l)
);

Saturate#

This formula uses saturation + (100% - saturation) * v to increase saturation from the current saturation to full. A value of 0 will have no effect and a value of 1 will be 100% saturated.

before
after

CSS Snippet
/* 0 (no change) to 1 (full) */
hsl(
  var(--color-h),
  calc(var(--color-s) + (100% - var(--color-s)) * 0.9),
  var(--color-l)
);

Desaturate#

This formula uses saturation - (saturation * v) to decrease saturation from the current saturation to grayscale. A value of 0 will have no effect and a value of 1 will be entirely grayscale.

before
after

CSS Snippet
/* 0 (no change) to 1 (grayscale) */
hsl(
  var(--color-h),
  calc(var(--color-s) - var(--color-s) * 0.6),
  var(--color-l)
);

Grayscale#

A color can be converted to grayscale by setting the saturation component to 0%.

before
after

CSS Snippet
hsl(
  var(--color-h),
  0%, /* Remove saturation component */
  var(--color-l)
);

Complement#

The compliment of a color can be calculated by rotating the hue 180 degrees.

before
after

CSS Snippet
hsl(
  calc(var(--color-h) + 180),
  var(--color-s),
  var(--color-l)
);

Invert (BONUS)#

This is the only SASS color function we cannot using the HSL model. But, if you really need to calculate a color's inverse, we can define additional RGB variables for our color and use them to calculate the inverse.

before
after

CSS Snippet
:root {
  --color-r: 65;
  --color-g: 153;
  --color-b: 225;
}

rgb(
  calc(255 - var(--color-r)),
  calc(255 - var(--color-g)),
  calc(255 - var(--color-b))
);

Why Do This?#

We've just seen how vanilla CSS can be used to manipulate colors but why wouldn't we just use a preprocessor? Well, even though CSS is more verbose, there are some benefits.

1) Dynamic theming:
The largest benefit of CSS variables variables is the ability to change them at runtime using JavaScript1. This makes adding dark mode or user-customizable themes trivial, for example. In fact, Insomnia relies heavily on CSS variables for it's theme system.

2) Scoped variables:
We saw in the previous example how we generated a red alert by simply changing the hue variable within the .alert.danger selector. This is because CSS variables are scoped. Since SASS does not support scoping like this, we'd have to add complexity by using mixins instead.

3) Preprocessors are a pain:
Not having to setup a preprocessor like SASS can be a huge benefit for simple projects and beginners. There are also some projects where a preprocessing step won't be an option at all.

So, with a little creative thinking, we were able to implement all off SASS's color manipulation functions using CSS variables and calc(). Perhaps one day CSS will have these functions build in but, until then, this will suffice.


Awesome, thanks for the feedback! 🤗

How did you like the article?