Easy Theming with OKLCH colors

Picture yourself with a CSS theme or framework that needs to be adapted to the Corporate Designs of several customers. You do not want to re-invent the wheel again and again and you’ve got more important things to do than overwrite dozens and dozens of color definitions.

What if I told you you can set a theme color for your whole stylesheet with a single CSS custom property, complete with contrast color, triadic color space (if you want), dark mode, colored checkboxes and so on? If that sounds intriguing, then read on for the full story!

A Very Quick Primer on oklch()

What’s making the new color keyword oklch() so special to bring it into all major browsers in record time is detailed elsewhere already. Therefore we won’t go into details about that anymore here.

Just a quick refresher on the syntax, because we’ll need it in a moment. The oklch() color function can be used in any place, where CSS allows or expects a color, from background-color over linear-gradient() to the new color-mix() function. It contains three or four values:

oklch(L C H)
/* or */
oklch(L C H / A)

where L is the perceived lightness and C the chroma, the “amount of color” so to say. A is optionally an amount of transparency.

This post is mainly about the H part, the hue or shade of color. We won’t go into details about L, C or A and just assume them as given.

Our Problem: Setting the Color

We start with defining exactly one variable by extracting the hue from the CD color of a customer. For the sake of argument, let’s set it to 130°, a bit blue-ish green on the color wheel:

:root {
--hue: 130deg;
}
You are here!

What Can We Do with It now?

Now we can tuck this property into any color definition to retain our original tint. We set the accent-color property to immediately colorize checkboxes and focus rings and set the font color to a dark color of the same hue.

body {
accent-color: oklch(
0.45 /* L, medium luminosity */
0.2 /* C, medium tint */
var(--hue) /* our hue from above */
);
color: oklch(
0.10 /* L, very little luminosity */
0.01 /* C, only use a little color */
var(--hue) /* again, our hue from above */
);
}

What’s a design without the use of contrast colors, though! Let’s make button backgrounds in the contrast color of our --hue. We define a new custom property for this:

:root {
--contrast-hue: calc(var(--hue) + 180deg);
}

and lo! this works! Even in the case where the calculated value exceeds 360° browsers are savvy enough to wrap the values around.

Now it’s a breeze to define our contrast-colored buttons:

button {
background: oklch(0.2 0.2 var(--contrast-hue));
color: white;
}

Color Schemes and Beyond

In the same way we can define whole color schemes depending on our initial hue definition:

:root {
--triadic-left-hue: calc(var(--hue) - 120deg);
--triadic-right-hue: calc(var(--hue) + 120deg);
}

We can go wild with CSS maths and also do some min() or max() or trigonometry, if that takes our fancy.

And Why Exactly oklch()?

The nice thing about using oklch() for this stunt is, that this color system tries to keep the perceived lightness even for all hues, a feature that is woefully missing from the older hsl() color function.

This allows us to play around with hues independently of the L and C values in our oklch() functions.

A Quick Demo of Its Powers

To demonstrate how easy it is to adapt the colors in a design, take this slider and try setting the hue yourself. Changing the slider will set the hue in the example box below.

Lorem ipsum dolor sit amet.

A checkbox

A sample palette of colors:

The lighting levels are taken from this blog post

Super-Charging Dark Mode

In the intro I talked about dark mode. Now, what about that? Turns out, if we set our lighting levels with custom properties, too, we can easily switch between light and dark mode:

:root {
--lighing-primary: 0.97;
--lighting-secondary: 0.12;
background: oklch(var(--lighting-primary) 0.02 var(--hue));
color: oklch(var(--lighting-secondary) 0.02 var(--hue));
}
@media (prefers-color-scheme: dark) {
:root {
--lighing-primary: 0.12;
--lighting-secondary: 0.97;
}
}

This is all that is needed for a consistent dark mode. Using oklch() and our defined custom properties consistently throughout your CSS makes this kind of adaption a breeze. With a little bit of imagination and sensitivity you can expand this solution to high-contrast modes and other accessibility enhancements, too.

In practice you’ll need to define more than just two levels of lighting. But even with ten lighting levels, your dark mode is implemented in 14 lines of CSS. Sounds like a good deal.

I hope I was able to get you as excited about the new possibilities with these CSS features as I am right now. It is truly remarkable what has become possible in the last years with vanilla CSS alone and across all major browsers.