Theming with CSS Custom Properties (variables) and calc()

Kjartan Rekdal Müller
ITNEXT
Published in
6 min readMay 13, 2019

--

Theming of styling has a lot of usecases. Component systems, customization, white labeling, dark mode/light mode, differentiation within an application (routes, topics, sub apps), accessibility.

A good theming system need to handle DRY (don’t repeat yourself), SOC (separation of concerns), loose coupling between parts of system (if I change something here, do I need to change something there?), and what I here will call co-variation (property-value-pairs that need to change together, like button background and button border color). If not these concerns are handled well, that will reflect on an increase in quality issues over time.

I will not discuss SOC here. A premise for this text is that CSS is the better way for SOC with regard to styling. What I will discuss is DRY, and coupling, but mostly co-variation. The last thing is the juicy part where I show how co-variation can be handled by vanilla CSS using custom properties and calc().

Step 1, Naive theming with cascade

First, let consider the naive version of theming:

Themes are created using cascade. But this is problematic for two reasons. If you need to update the color, you will have to change it everywhere the theme color is used within the CSS. This is not just a case of ‘find and replace’ when the CSS grows. What if the sameness is a coincidence? (Yes, that happens.) Or if RGB and HSL is used in a mix?

The other thing is that if a theme is added, you probably need to update the CSS for the individual components as well, and this introduces hard coupling within the system.

Step 2, Naive theming with custom properties

So to start with, you could use custom properties to ensure that the same value is used where it should be, and that it could be changed from somewhere central:

Here you at least make sure that you safely can update colors and DRY. But you still need to handle the coupling. To do that, you could avoid using properties that are named after their values, and for example use a named color scheme as part of your system. A color scheme could be something like primary and secondary color, or theme color and brand color, or something. That is up to you.

Step 3, Not so naive theming with custom properties

So instead of referencing a custom property for a specific color, the components reference a named custom property from the color scheme, and this again can be changed from somewhere central without bothering the component. And adding themes will be a breeze:

Here the component references the propert --themecolor, and the value of that property is kind of set and scoped by the theme-class. The added benefit is readability, since you now communicate the intent or role of the property. You can still use custom properties for named values in addition. And that would probably be good practice, since it reduces the risk of accidentally breaking the theming while adding and changing stuff. You just assign the named value-property to the schemed-property:

--themecolor: var(--purple);

Step 4, Co-variation with calc()

All well so far, but what about co-variation? The thing is that custom properties can be used together with calc(). A usecase could be a top and side margin that need to be in a certain ratio:

--marginSides: 30px;
--marginTopBottom: calc(var(--marginSides) * .7)

If you change --marginSides, the marginTopBottom will follow. So by using calc(), you can have co-variation in your system without pre-processors like LESS and SASS.

And this also goes for colors. My favorite LESS color function is darken(), and, well, could we do something like that in plain CSS? Yes, of course, and the secret to that is using HSL instead of RGB. HSL is great because it separate the color into properties that are easier to manipulate than RGB, and each of those colors can be represented by custom properties:

--hue: 255;
--saturation: 100%;
--lumnosity: 87%;
--themecolor: hsla(var(--hue), var(--saturation), var(--lumnosity),1);

And this could be used for theming and co-variation between color values, like a slighly darker border for some background color:

Darkening of a color can be done by changing saturation and lumnosity using calc(), and in that way co-variation of shades can be handled by the system. You need 50 shades? No problem!

And, as you can see in this Pen, a nice touch is that you also can use custom properties for shorthand properties, which greatly reduces the risk of accidentally introducing variability within the system:

--themeBorder: var(--borderSize) solid var(--darkThemecolor);border: var(--themeBorder);

You can still overwrite specific properties within the components if you need:

.cracyButton {
border: var(--themeBorder);
border-left: 15px dotted var(--cracyColor);
}

Step 5, Make sure that rules are calculated as they should

Custom properties are scoped to the element(s) they are declared on, and participate in the cascade: the value of such a custom property is that from the declaration decided by the cascading algorithm. https://developer.mozilla.org/en-US/docs/Web/CSS/--*

The trick to using custom properties and calc() with theming, is that a CSS rule is not automatically updated if a property is changed, so if a property used in calc() is changed, you have to make sure that the rule is recalculated.

So something like this won’t work, since changing the property down in the DOM-tree does not automatically affect the .themed-rule higher up:

//Does not work.themed {
--hue: var(--basichue);
--themecolor: hsla(calc(var(--hue), ...);
}
.procolTheme {
--hue: var(--aWhiterShadeOfPale);
}
.hendrixTheme {
--hue: var(--purpleHaze);
}
<main class="themed">
<section class="procolTheme">...</section>
<section class="hendrixTheme">...</section>
</main>

Changing the order, or having them on the same node, as in the Pen, will help.

In addition there are possible strategies for forcing a recalculation of the calculating rule. The basic principle is to do something that updates the property within the scope. You could add a rule later in the flow that partly updates the rule. If the usecase is theme per route, you can have a shared CSS with basic theming, and a per route CSS that overwrites the rule with just the needed properties. Or the overwriting CSS can be in the components if routing is handled by the framework, or if you just need to handle different themes within the application for another reason. It is also possible to add the overwriting rule by JavaScript.

If the usecase is dark-/lightmode and the browsers at that point all supports the media query prefers-color-scheme , you can trigger recalculation within the media query:

@media (prefers-color-scheme: dark) {
.themed {
--hue: var(--backInBlack);
}
}

And as a sidenote here: Custom properties and media queries are great for handling responsive design in a theming system, since you can really minimize the needed code within the queries!

Step 6, Over the top

The last one is more experimental than practical, but shows that you can implement a colorwheel-based colorscheme as co-variation. But your designers will probably not thank you for it, since it misses the finetuning that make their day.

The scheme used in this Pen, is known as complementary split:

Complimentary split: http://www.paletton.com/wiki/index.php?title=Split_complementary_color_scheme

Since hue can be seen as an angle in the color wheel (0–360 degrees), it is easy to derive the other colors in the scheme in the same way that you can manipulate saturation and lumnosity:

--primaryHue: var(--hueBasic);
--secondaryHue: calc(var(--primaryHue) + 150);
--complementaryHue: calc(var(--primaryHue) + 210);

And don’t worry if you get values > 360, it just starts another round.

Summary

So to sum it up. CSS custom properties and calc() can help you dealing with sameness, coupling and co-variation within a theming system.

Sameness, since custom properties can define values that need to be the same throughout the system. Coupling, since custom properties can be defined by their role, not only as shorthand for their value. Co-variation can be handled by a combination of custom properties and calc() to bind values together, since calc() can use custom properties as parameters.

--

--

Team Lead at NEP Norway and developer/architect. Creative technologist with PhD in Digital genre- and platform development and design.