A Guide To Modern CSS Colors

A Guide To Modern CSS Colors

There’s more to color on the web than meets the eye, and it’s about to get a lot more interesting! Today, we’ll take a look at the best ways to use colors in a design system, and what we can expect from our colors in the not-too-distant future.

Well-Known Color Values

There are many different ways to define colors in CSS. CSS named colors are one of the simplest ways to color an element:

.my-element {
  background-color: red;
}

These are very limited, and rarely fit the designs we are building! We could also use color hex (hexadecimal) values. This code gives our element a red background color:

.my-element {
  background-color: #ff0000;
}

Unless you’re a color expert, hex values are very difficult to read. It’s unlikely you would be able to guess the color of an element by reading the hex value. When building a website we might be given a hex color value by a designer, but if they asked us to make it, say 20% darker, we would have a hard time doing that by adjusting the hex value, without a visual guide or color picker.

RGB

RGB (red, green, blue) notation is an alternative way of writing colors, giving us access to the same range of colors as hex values, in a much more readable form. We have an rgb() function in CSS for this. Colors on the web are additive, meaning the higher the proportion of red, green and blue, the lighter the resulting color will be. If we only use the red channel, the result is red:

.my-element {
  background-color: rgb(255, 0, 0);
}

Setting the red, green and blue channels to the highest value will result in white:

.my-element {
  background-color: rgb(255, 255, 255);
}

We can also add an alpha channel (for transparency), by using the rgba() function:

.my-element {
  background-color: rgba(255, 0, 0, 0.5); // transparency of 50%
}

.my-element {
  background-color: rgba(255, 0, 0, 1); // fully opaque
}

rgb() and rgba() allow us to “mix” colors in our code to some extent, but the results can be somewhat unpredictable.

HSL

More recently, we have been able to use HSL (hue, saturation, lightness) values, with the hsl() and hsla() color functions. As a developer, these are far more intuitive when it comes to adjusting color values. For example, we can get darker and lighter variants of the same color by adjusting the lightness parameter:

.my-element {
  background-color: hsl(0deg, 100%, 20%); // dark red
}

.my-element {
  background-color: hsl(0deg, 100%, 50%); // medium red
}

.my-element {
  background-color: hsl(0deg, 100%, 80%); // light red
}

The hue parameter represents the position on a color wheel, and can be any value between 0 and 360deg. The function also accepts turn units (e.g. 0.5turn), and unitless values.

The following are all valid:

.my-element {
  background-color: hsl(180deg, 50%, 50%);
}

.my-element {
  background-color: hsl(0.5turn, 50%, 50%);
}

.my-element {
  background-color: hsl(180, 50%, 50%);
}

Tip: Holding down SHIFT and clicking the color swatch in the inspector in Chrome and Firefox dev tools will toggle the color value between hex, RGB and HSL!

hsl() and hsla() lend themselves well to manipulation with custom properties, as we’ll see shortly.

currentColor

The currentColor keyword is worth a mention as another way of setting a color on an element that’s been around for a while. It effectively allows us to use the current text color of an element as a variable. It’s pretty limited when compared with custom properties, but it’s often used for setting the fill color of SVG icons, to ensure they match the text color of their parent. Read about it here.

Modern Color Syntax

The CSS Color Module Level 4 provides us with a more convenient syntax for our color functions, which is widely supported in browsers. We no longer need the values to be comma-separated, and the rgb() and hsl() functions can take an optional alpha parameter, separated with a forward slash:

.my-element {
  /* optional alpha value gives us 50% opacity */
  background-color: hsl(0 100% 50% / 0.5);
}

.my-element {
  /* With no alpha value the background is fully opaque*/
  background-color: hsl(0 100% 50%);
}
New CSS Color Functions

HWB

HWB stands for hue, whiteness and blackness. Like HSL, the hue can be anywhere within a range of 0 to 360. The other two arguments control how much white or black is mixed into that hue, up to 100% (which would result in a totally white or totally black color). If equal amounts of white and black are mixed in, the color becomes increasingly gray. We can think of this as being similar to mixing paint. It could be especially useful for creating monochrome color palettes

Try it out with this demo (works in Safari only):

Why do we need LAB and LCH when we have HSL? One reason is that using LAB or LCH, gives us access to a much larger range of colors. LCH and LAB are designed to give us access to the entire spectrum of human vision. Furthermore, HSL and RGB have a few shortcomings: they are not perceptually uniform and, in HSL, increasing or decreasing the lightness has quite a different effect depending on the hue.

In this demo, we can see a stark contrast between LCH and HSL by hitting the grayscale toggle. For the HSL hue and saturation strips, there are clear differences in the perceptual lightness of each square, even though the “lightness” component of the HSL function is the same! Meanwhile, the chroma and hue strips on the LCH side have an almost-uniform perceptual lightness.

We can also see a big difference when using LCH color for gradients. Both these gradients start and end with the same color (with LCH values converted to the HSL equivalents using this converter). But the LCH gradient goes through vibrant shades of blue and purple in the middle, whereas the HSL gradient looks muddier and washed-out by comparison.

  • See the demo →

LAB and LCH, while perhaps being syntactically a little less intuitive, behave in a way that makes more sense to the human eye. In her article, LCH color in CSS: what, why, and how?, Lea Verou explains in detail the advantages of LCH color. She also built this LCH color picker.

As with other color functions, hwb(), lab() and lch() can also take an optional alpha parameter.

.my-element {
  background-color: lch(80% 240 50 / 0.5); // Resulting color has 50% opacity
}
Browser Support And Color Spaces

hwb(), lab() and lch() are currently only supported in Safari. It’s possible to start using them straight away by providing a fallback for non-supporting browsers. Browsers that don’t support the color function will simple ignore the second rule:

.my-element {
  background-color: lch(55% 102 360);

  /* LCH color converted to RGB using Lea Verou’s tool: https://css.land/lch/ */
  background-color: rgb(98.38% 0% 53.33%);
}

If other styles depend on newer color functions being supported, we could use a feature query:

.my-element {
  display: none;
}

/* Only display this element if the browser supports lch() */
@supports (background-color: lch(55% 102 360)) {
  .my-element {
    display: block;
    background-color: lch(55% 102 360);
  }
}

It’s worth noting, as Lea explains in her article, that although modern screens are capable of displaying colors beyond RGB, most browsers currently only support colors within the sRGB color space. In the LAB color demo you might notice that moving the sliders beyond a certain point doesn’t actually affect the color, even in Safari where lab() and lch() are supported. Using values outside of the sRGB range will only have an effect when hardware and browsers advance sufficiently.

Safari now supports the color() function, which enables us to display colors in the P3 space, but these are limited to RGB colors for now, and don’t yet give us all the advantages of LAB and LCH.

.my-element {
  background: rgb(98.38% 0% 53.33%); // bright pink
  background: color(display-p3 0.947 0 0.5295); // equivalent in P3 color space
}

Recommended Reading: “Wide Gamut Color in CSS with Display-P3” by Nikita Vasilyev

Accessibility

Once they are widely supported, perhaps LAB and LCH can help us choose more accessible color combinations. Foreground text should have the same contrast ratio with background colors with different hue or chroma values, as long as their lightness value remains the same. That’s certainly not the case at the moment with HSL colors.

Color Management

A wider range of color functions means we have more options when it comes to managing colors in our application. Often we require several variants of a given color in our design system, ranging from dark to light.

Custom Properties

CSS custom properties allow us to store values for reuse in our stylesheets. As they allow partial property values, they can be especially useful for managing and manipulating color values. HSL lends itself particularly well to custom properties, due to its intuitiveness. In the previous demo, I’m using them to adjust the hue for each segment of the color strip by calculating a --hue value based on the element’s index (defined in another custom property).

li {
  --hue: calc(var(--i) * (360 / 10));
  background: hsl(var(--hue, 0) 50% 45%);
}

We can also do things like calculate complementary colors (colors from opposite sides of the color wheel). Plenty has been written about this, so I won’t cover old ground here, but if you’re curious then Sara Soueidan’s article on color management with HSL is a great place to start.

Migrating From Hex/RGB To HSL

RGB colors might serve your needs up to a point, but if you need the flexibility to be able to derive new shades from your base color palette then you might be better off switching to HSL (or LCH, once supported). I would recommend embracing custom properties for this.

Note: There are plenty of online resources for converting hex or RGB values to HSL (here’s one example).

Perhaps you have colors stored as Sass variables:

$primary: rgb(141 66 245);

When converting to HSL, we can assign custom properties for the hue, saturation and lightness values. This makes it easy to create darker or lighter, more or less saturated variants of the original color.

:root {
  --h: 265;
  --s: 70%;
  --l: 50%;

  --primary: hsl(var(--h) var(--s) var(--l));
  --primaryDark: hsl(var(--h) var(--s) 35%);
  --primaryLight: hsl(var(--h) var(--s) 75%);
}

HSL can be incredibly useful for creating color schemes, as detailed in the article Building a Color Scheme by Adam Argyle. In the article he creates light, dark and dim color schemes, using a brand color as a base. I like this approach because it allows for some fine-grained control over the color variant (for example, decreasing the saturation for colors in the “dark” scheme), but still retains the big advantage of custom properties: updating the brand color in just one place will be carried through to all the color schemes, so it could potentially save us a lot of work in the future.

Sass Color Functions

When it comes to mixing and adjusting colors, Sass has provided color functions to enable us to do just this for many years. We can saturate or desaturate, lighten or darken, even mix two colors together. These work great in some cases, but they have some limitations: firstly, we can only use them at compile-time, not for manipulating colors live in the browser. Secondly, they are limited to RGB and HSL, so they suffer from the same issues of perceptual uniformity, as we can see in this demo, where a color is increasingly desaturated yet appears increasingly lighter when converted to grayscale.

To ensure that the lightness remains uniform, we could use custom properties with LCH in a similar way to HSL above.

li {
  --hue: calc(var(--i) * (360 / 10));
  background: lch(50% 45 var(--hue, 0));
}
Color Mixing And Manipulation

Color Mixing

One thing CSS doesn’t yet allow us to do is mix colors in the browser. That’s all about to change: the Level 5 CSS Color Specification (working draft) contains proposals for color mixing functions that sound rather promising. The first is the color-mix() function, which mixes two colors much like Sass’s mix() function. But color-mix() in CSS allows us to specify a color space, and uses the LCH by default, with superior mixing as a result. The colors don’t have to be LCH when passed in as arguments either, but the interpolation will use the specified color space. We can specify how much of each color to mix, similar to gradient stops:

.my-element {
  /* equal amounts of red and blue */
  background-color: color-mix(in lch, red, blue);
}

.my-element {
  /* 30% red, 70% blue */
  background-color: color-mix(in lch, red 30%, blue);
}

Color Contrast And Accessibility

color-contrast() is another proposed function, which really does have huge implications for picking accessible colors. In fact, it’s designed with accessibility in mind first and foremost. It permits the browser to pick the most appropriate value from a list, by comparing it with another color. We can even specify the desired contrast ratio to ensure our color schemes meet WCAG guidelines. Colors are evaluated from left to right, and the browser picks the first color from the list that meets the desired ratio. If no colors meet the ratio, the chosen color will be the one with the highest contrast.

.my-element {
  color: wheat;
  background-color: color-contrast(wheat vs bisque, darkgoldenrod, olive, sienna, darkgreen, maroon to AA);
}

Because this isn’t supported in any browsers right now, I’ve borrowed this example directly from the spec. when the browser evaluates the expression the resulting color will be darkgreen, as it is the first one that meets the AA contrast ratio when compared to wheat, the color of the text.

Browser Support

The Level 5 Color Specification is currently in Working Draft, meaning no browsers yet support the color-contrast() and color-mix() functions and their syntax is subject to change. But it certainly looks like a bright future for color on the web!

Environmental Impact Of Colors

Did you know that your chosen color palette can have an impact on how much energy your website uses? On OLED screens (which account for most modern TVs and laptops), darker colors will use significantly less energy than light colors — with white using the most energy, and black the least. According to Tom Greenwood, author of Sustainable Web Design, blue is also more energy-intensive than colors in the red and green areas of the spectrum. To reduce the environmental impact of your applications, consider a darker color scheme, using less blue, or enabling a dark-mode option for your users. As an added bonus, a more environmentally friendly choice of colors can also reduce the impact on the battery life of mobile devices.

Tools
  • Hexplorer, Rob DiMarzo
    Learn to understand hex colors with this interactive visualization.
  • LCH color picker, Lea Verou and Chris Lilley
    Get LCH colors and their RGB counterparts.
  • HWB color picker
    Visualize HWB colors and convert to HSL, RGB and hex.
  • Ally Color Tokens, Stephanie Eckles
    An accessible color token generator.
Resources
  • “A Nerd’s Guide To Color On The Web,” Sarah Drasner, CSS-Tricks
  • “LCH Colors In CSS: What, Why, And How?,” Lea Verou
  • “The Best Color Functions In CSS?,” Chris Coyier, CSS-Tricks
  • “Building A Color Scheme,” Adam Argyle, Web.dev
  • “Using HSL Colors In CSS,” Ahmad Shaheed, Smashing Magazine
  • “On Switching From Hex And RGB To HSL,” Sara Soueidan
  • “Improving Color On The Web,” Dean Jackson, Webkit Blog