Creating Color Shades with CSS Custom Properties

September 5th, 2023
A heard of sheep standing on a hill

CSS Custom Properties, also known as CSS variables, are a great way to define values that you can reuse throughout your stylesheets. It has become a common pattern for developers to define their variables within the scope of the root element, so that they are accessible anywhere on their site.

:root {
  --main-bg-color: darkslategray;
}

In this example, we have defined a custom property called '—main-bg-color' and assigned its value to darkslategray. When defining a CSS variable, the property name must begin with a double hyphen, and the value can be any valid CSS value. After declaring this custom property, we can access its value with the var function.

.my-element {
  background: var(--main-bg-color);
}

This pattern of declaring custom properties and using them throughout your site's stylesheets is a great way to keep your code DRY. Let's say we've built a site using CSS variables to store the theme colors and then we are suddenly tasked with updating the colors of the site. Instead of having to replace each instance of a hexcode or static color, we can update the value once where we defined the custom property and then it will update the color everywhere on the site.

There is one problem with this approach though. When building modern web applications, we will likely need several colors and various shades of each color. For example, let's say we wanted to make a primary button that uses the main theme color of our site. We might need the button to change colors to a lighter shade when the user hovers their mouse over the button. And we might want yet another shade of the primary color to appear when the user is actively clicking on sed button. In an ideal world, we could still define just one custom property for the primary color, but still be able to lighten or darken it as needed… Let's look at a couple ways we could accomplish this.

hsl()

The first option we have for creating color shades with CSS properties is to use the HSL syntax whenever defining colors. To do this, we'll have to separate our color into a couple different custom properties.

:root {
  --primary-color-h: 217;
  --primary-color-s: 90%;
  --primary-color-l: 61%;

  --primary-color: hsl(
    var(--primary-color-h)
    var(--primary-color-s)
    var(--primary-color-s)
  );
}

Now we can style a button to have a lighter or darker shade of the primary color like this:

button {
  background: var(--primary-color);
}

button:hover {
  background: hsl(
    var(--primary-color-h)
    var(--primary-color-s)
    calc(var(--primary-color-l) + 10%)
  );
}

All we have to do to lighten our color is use the calc function to increase our lightness variable by 10%. If this seems like a lot of code just to get a color, you could also combine the hue and saturation into one variable to save yourself some typing.

:root {
  --color: 217 90%;
  --l: 61%;

  --primary-color: hsl(var(--color) var(--l));
}

Using the hsl color notation is widely supported by browsers and is probably the technique I would recommend using for production applications.

color-mix()

The second method we'll look at using is the color-mix function. In my opinion, this function provides neater syntax than using HSL colors, but its browser support is not as good. According to caniuse, this CSS function is supported in about 81% of all browsers globally.

The color-mix function works by taking two colors and mixing them together, resulting in a new color. So to lighten any color we could mix it with white, and to darken any color we could mix it with black.

:root {
  color: #f4527b;
}

button {
  background: var(--color);
}

button:hover {
  background: color-mix(in srgb, var(--color), #000 10%);
}

The first parameter we pass to the color-mix function is an interpolation method. This represents the color space used for interpolation between color values. The next two parameters are the two colors we want to mix, and how much of each we want to mix. I like to imagine we have an empty paint can, and have to specify how much of each color paint we want to put in the can. In this example, we would be filling our paint can with black paint (#000) and with the color that we specified in our custom property (#f4527b). We specified that we want to fill the can 10% of the way up with the black paint, and since we didn't specify a percentage for the other color, it will automatically fill up the remaining amount - 90%. Now if we mix 90% of our color with 10% black, we get a slightly darker version of our original color. Pretty nifty, right?! And if we wanted to lighten a color, all we have to do is mix it with a little bit of white instead.

If you would like to see theses two shading methods in action, I've created a simple codepen to demonstrate both techniques.