Can you teach an old design system new colours?

This article, part of the writing collection, was published on .

Over the years, I’ve changed the colours used across my website a number of times, and I’m happy with what I have now, but what I’m going to talk about today are formats for defining colours and some recent behind-the-scenes changes I made to how I define colours on my website, making theming my website easier.

Hexidecimal / RGB Permalink

  • Raven
    #5f8aa6
  • Thunder
    #060606
  • Snowy
    #f9f9f9

For many years, my colours were defined in Hexidecimal format, or Hex as it’s commonly known as, which maps to the RGB colour model. This was essentially the de facto way of defining colours back when I started web development in 2008.

But when it comes to reading and understanding Hex and RGB colours, it’s a bit more tricky, particularly when dealing with the hexidecimal numbering system.

Unfortunately, this demo requires JavaScript to function correctly!

Using the sliders above, try to make this blue.

Now try to make it this pink.

This is absolutely not an easy task! This is because all three values, in tandem, are responsible for controlling not just the hue, but also the saturation and the lightness of that hue. This makes authoring and reading Hex and RGB values difficult as it’s difficult to imagine how certain ratios and amounts of each value mix to produce a colour.

RGB is an additive colour format. This means that, starting from black, we combine red, green, and blue together to create a colour, and the syntax we use when coding to define Hex/RGB colours follows this format: we have to provide a red, green, and blue value.

These limitations, particularly in creating (or finding) colours, were probably a good thing for web development, as it spawned countless, incredible tools that presented Hex/RGB in ways that are generally easier to understand and manipulate for humans than manually adjusting RGB values. This almost-surely pushed the collective understanding of colour on the web further and helped ingratiate people towards alternative and newer formats for defining colour.

HSL Permalink

  • Raven
    hsl(204 29% 51%)
  • Thunder
    hsl(0 0% 2%)
  • Snowy
    hsl(0 0% 98%)

Enter HSL. Coming from the world of Hex/RGB, it was immediately more-readable to me, where the values in the HSL syntax tell us: which direction in the colour wheel to point; how saturated (how much) of that colour we want, and the lightness of the produced colour.

It’s important to know that HSL uses the same sRGB colour space as Hex and RGB, it just provides different parameters for us as developers to achieve the same colours.

Unfortunately, this demo requires JavaScript to function correctly!

Like before, using the sliders above, try to make this blue.

Then try to make it this pink.

It may not necessarily be quicker to do, but the inputs that we get with HSL are certainly more intuitive to work with and comprehend than RGB’s. Importantly, it puts two of the ways that we typically describe colour with language, by a colour’s Hue and Lightness, as two of the colour format’s inputs (which, I imagine maps to parts of the brain more easily than RGB). The third input, Saturation may not be immediately obvious, but can be understood by playing with its value.

Hue

In contrast to manipulating RGB values, finding the Hue of your target colour is much easier here; although, not perfect, it’s still not trivial to be able to remember how much rotation is required to find your target Hue.

Setting the Saturation to 100% and Lightness to 50%, and then cycling through the values for Hue, from 0° to 360° will give you an idea of what colours look like in their most saturated and untinted/unshaded form.

Saturation

Next is Saturation which dictates how much of our given Hue we want.

With any Hue, try setting the Lightness to 50%, and then cycling through the values for Saturation, from 0% to 100%. This will give you an idea of how the amount of Saturation we apply to Hue dictates how far away from being completely-greyscale we make the colour. You’ll also notice that when the Saturation is at 0%, it doesn’t matter what the Hue is anymore—given the same Lightness, an unsaturated Red Hue and an unsaturated Cyan Hue appear the same.

Lightness

The easiest way for me to understand the Lightness value is to think of it like a slide between black and white. The midpoint at 50% mixes your colour with both black and white (so a grey colour that is perfectly between black and white). As you deviate away from the midpoint, your colour is mixed with either more black and less white (as Lightness decreases) or more white and less black (as Lightness increases), reaching a point of being pure black at 0% and pure white at 100%.

You can see this effect no matter what the Hue and Saturation values are.

Problems with the sRGB colour space Permalink

Inconsistent perceived lightness

While one of the parameters of an HSL colour is Lightness, you’ll find that there is a stark contrast in how we perceive the brightness of Hues around the wheel.

Cyan and blue both have identical Saturation and Lightness values—only their Hues differ—but we perceive the cyan as being much brighter than the blue. We can account for this by changing Saturation and Lightness to compensate, but an ideal colour space would provide consistency in its parameters.

Dead grey zones

A clear example of this is in displaying gradients; when instructing the browser to traverse the colour space to find a line between two colours that will make up the gradient, you’ll often find that it takes a path that you don’t expect or want. It’s not uncommon to find that developers have created gradients that don’t simply traverse from point A to point B in the colour space; they provide additional colour stops to ensure that the gradient looks correct.

In the example, I’ve instructed the browser to create a gradient between red and cyan, but you’ll notice that it goes through a grey/monochrome area of the colour space around the midpoint. I would hope that the produced gradient would tween between not just the hues of red and cyan, but also their lightness and saturation. It seems, however, that the path taken to draw gradients often goes through parts of the sRGB colour space that have little or no saturation, leading to dead grey zones.

OKLCH Permalink

  • Raven
    oklch(61.29% 0.064 237.73)
  • Thunder
    oklch(15% 0 0)
  • Snowy
    oklch(98.2% 0 0)

And finally, where I’ve happily landed: OKLCH. This is the most flexible and understandable colour format that I’ve found, but do check out the list of predfined colour spaces in the CSS Color Specifications for all the possibilities.

Unfortunately, this demo requires JavaScript to function correctly!

Last time now, using the sliders above, try to make this blue.

Finally, try to make it this pink.

This is about on the same level as HSL in terms of authoring and reading, but OKLCH fixes a number of problems with the sRGB colour space that makes it more consistent and reliable across its colour space. In addition, it offers both more colours and colours that are outside of the sRGB colour space, i.e. colours that were not achievable on the web before.

Browser Support

Desktop support:

  • Chrome: 111
  • Edge: 111
  • Firefox: 113
  • Opera: 97
  • Safari: 15.4

Mobile support:

  • Android Browser: 111
  • Chrome (Android): 111
  • Firefox (Android): 113
  • Safari (iOS): 15.4
  • Samsung Internet: 22.0

oklch

Browser support data for oklch comes from MDN’s browser-compat-data and is up-to-date as of version 5.5.24.

Lightness

Lightness here functions similarly to the HSL Lightness value in that it mixes your colour with different levels of black and white.

You can see this effect no matter what the Chroma and Hue values are.

Chroma

Chroma is similar, again, to HSL’s Saturation; it dictates how much of our given Hue we want. What differs from HSL’s Saturation is that even when the Lightness is zero, we still get some colour, depending on how much Chroma we apply. So in this colour space, greyscale colours, including black and white, require that the Chroma value be 0.

You’ll also notice that, like with HSL’s Saturation, when the Chroma is at 0%, it doesn’t matter what the Hue is anymore—given the same Lightness values and zero Chroma, changing Hue no longer affects the produced colour.

Hue

OKLCH’s Hue functions very much the same as HSL’s Hue, but the values don’t line up exactly the same. This is because unlike Hex/RGB and HSL, OKLCH operates in a different colour space and actually makes available (to supporting browsers and devices) even more colours than were available in the sRGB colour space.

Try setting the Lightness to 50% and the Chroma to 100%, and then cycling through the values for Hue from 0° to 360°. This will give you an idea of what colours look like in their most saturated and untinted/unshaded form.

Colours in practice Permalink

This all came about when I decided that I wanted to add a Sepia theme to my website (check out the theme selector in the footer!), but I didn't have a clean and concise way to go about it. What I wanted was a way to slightly-tint all the colours on my website into the sepia range.

Until recently, I had defined my colours manually, like this:

--color-raven: oklch(61.29% -0.034 -0.054);
--color-thunder: oklch(12.2% 0 0);
--color-snowy: oklch(98.2% 0 0);
--color-mineshaft: oklch(28.9% 0 0);
--color-yeti: oklch(89.8% 0 0);
--color-lynx: oklch(17.6% -0.01 -0.014);
--color-bear: oklch(96% -0.006 -0.01);
...

So building a theme where all of the colours are tinted sepia meant redefining each of the CSS Variables I had already defined for the default (non-sepia) theme.

That’s where the incredible new CSS function, color-mix(), comes in! Using it, I’ve been able to boil down my 13-colour palette into 3 core colours and 10 variations of those colours.

A pixel-art image showing how the three base colours mix to create ten variation colours in the thirteen-colour palette

The first step is to set up some initial CSS variables to use for building out the --color-* variables. These are the variables that we’ll later use to change themes, including our desired Sepia theme.

--snowy-lightness: 98.2%;
--thunder-lightness: 15%;
--monochrome-chroma: 0;
--monochrome-hue: 0;

--raven-lightness: 61.29%;
--raven-chroma: 0.064;
--raven-hue: 237.73;

Next, we’ll build out the 3 base colours in the palette using the above variables, and from these 3 base colours, the 10 remaining colours in the palette can be constructed:

  • Raven
    oklch(
    var(--raven-lightness)
    var(--raven-chroma)
    var(--raven-hue)
    )
  • Thunder
    oklch(
    var(--thunder-lightness)
    var(--monochrome-chroma)
    var(--monochrome-hue)
    )
  • Snowy
    oklch(
    var(--snowy-lightness)
    var(--monochrome-chroma)
    var(--monochrome-hue)
    )
  • Mineshaft
    color-mix(
    in oklab,
    var(--color-snowy),
    var(--color-thunder) 83.6%
    )
  • Kaiser
    color-mix(
    in oklab,
    var(--color-snowy),
    var(--color-thunder) 66.6%
    )
  • Nickel
    color-mix(
    in oklab,
    var(--color-snowy),
    var(--color-thunder) 51.8%
    )
  • Yeti
    color-mix(
    in oklab,
    var(--color-snowy),
    var(--color-thunder) 10.2%
    )
  • Lynx
    color-mix(
    in oklab,
    var(--color-raven),
    var(--color-thunder) 75%
    )
  • Wolf
    color-mix(
    in oklab,
    var(--color-raven),
    var(--color-thunder) 54%
    )
  • Bowhead
    color-mix(
    in oklab,
    var(--color-raven),
    var(--color-thunder) 33%
    )
  • Highland
    color-mix(
    in oklab,
    var(--color-raven),
    var(--color-snowy) 33%
    )
  • Coyote
    color-mix(
    in oklab,
    var(--color-raven),
    var(--color-snowy) 54%
    )
  • Bear
    color-mix(
    in oklab,
    var(--color-raven),
    var(--color-snowy) 75%
    )

Why am I mixing colours with in oklab?

I prefer the way that OKLab mixes colours and traverses the colour space for gradients; although, I still much prefer the syntax of defining colours themselves using OKLCH.

And with that, the 13-colour palette is complete!

Time to start putting all this work to use.

Applying a tint

Now that we have 3 base colours and 10 variations, built in the browser based on 7 variables, we can redefine just a few of those variables and affect the entire palette.

/* bring down the amount of white */
--snowy-lightness: 88%;

/* slight yellow tint to monochrome colours */
--monochrome-chroma: 0.06;
--monochrome-hue: 90;

/* slight yellow tint to raven-based colours */
--raven-chroma: 0.034;
--raven-hue: 180;

Sepia Theme

  • Raven
  • Thunder
  • Snowy
  • Mineshaft
  • Kaiser
  • Nickel
  • Yeti
  • Lynx
  • Wolf
  • Bowhead
  • Highland
  • Coyote
  • Bear

This makes it easy to change the theme of my website just by manipulating the raven-based variables. Critically, you might notice that the perceived brightness of the colours between these themes look much more consistant than what the RGB colour space provided.

Unfortunately, this demo requires JavaScript to function correctly!

  • Raven
  • Thunder
  • Snowy
  • Mineshaft
  • Kaiser
  • Nickel
  • Yeti
  • Lynx
  • Wolf
  • Bowhead
  • Highland
  • Coyote
  • Bear

Opacity vs. Transparency

I also have a set of defined opacities that I use with the opacity property throughout my CSS:

--mostly-opaque: 90%;
--barely-opaque: 10%;

opacity: var(--barely-opaque);

The first instinct here might be to generate even more CSS variables for every combination of colour and opacity that I have defined, but we can actually use color-mix() once again to reuse the CSS variables that we already have:

--mostly-opaque: 90%;
--barely-opaque: 10%;

background-color: color-mix(in oklab, var(--color-thunder), transparent calc(100% - var(--mostly-opaque)));

You’ll notice that I’m using calc() at the end to invert the opacity variable. This is because opacity and transparency are opposites; if something is opaque, it is not transparent! (Deep facepalm when I finally realised this.) So when we say we want something to be 90% opaque, it's the same as saying we want it to be 10% transparent, which the calc() function achieves for us.

Unfortunately, this demo requires JavaScript to function correctly!

Problems with new colour spaces Permalink

There are some limitations and gotchas to be aware of when using these new colour spaces; although, they have more to do with compatability and browser support than difficulty in describing and representing colour.

Fallbacks

First of all, older browsers that don’t support oklch() (see modern browser support), for example, will require fallbacks wherever you use newer colour formats:

/* put one of these first: */
color: #5f8aa6;
color: rgb(95, 138, 166);
color: hsl(204 29% 51%);
/* to fallback on from this, where the
entire line of CSS fails in
unsupported browsers: */
color: oklch(61.29% 0.064 237.73);

However, this does not work:

--color-raven: #5f8aa6;
--color-raven: oklch(61.29% 0.064 237.73);

Instead of what you’d hope would happen—older browsers ignore the second line—the --color-raven variable is instead always set to the second value, even if it isn’t supported and won’t work. This is also important if you plan on supporting older browsers and using color-mix() too (see modern browser support) because you’ll be unable to use the technique I’m using to combine colours.

You can use the amazing website, OKLCH Color Picker & Converter to calculate colours in the sRGB colour space that are relatively close to your OKLCH

Out of range

Secondly, there are areas of the new colour spaces you’ll find when using oklch(), oklab(), lch(), lab(), color(), etc., that are outside of the sRGB colour space, and therefore cannot be accurately represented in Hex, RGB, or HSL, for example.

You may also run into the issue of hardware being unable to present your vivid, new colours because they exist outside the colour space that a given display is built to show. I hope it’s only a matter of time before supporting hardware becomes more widespread and we can have more confidence that our colours will be presented to our website visitors just as we intend; although, I certainly dislike the waste involved in “upgrading” all those devices.

However, like with new and emerging web standards, without adopting these new colour spaces, we’ll be missing out on huge chunks of a beautiful spectrum that the web of today is already ready for!

Taking this further Permalink

I can’t overstate how much clearer HSL and OKLCH colour formats are to read and author compared to Hex and RGB. Unless you’re using a CSS preprocessor like Sass/SCSS, it can be laborious to calculate combinations of colours or create tints, particularly if the colours in your palette ever change.

Colour formats like HSL and OKLCH give us much more intuitive understanding of the nature of a given colour, and the ways to manipulate and create variations of those colours

I hope you found this useful, and I’d love to know how others are doing this sort of things or what kind of pitfalls I might run into using this technique!

Further Reading Permalink

You can also send an anonymous reply (using Quill and Comment Parade).

5 Responses

  1. 3 Likes
  2. 1 Repost
  3. 1 Link