Skip to content

Stateful Theming with CSS Custom Properties

Published on

For about a month I’ve been working on a client project that requires the use of different UI themes over different contexts.

After considering a couple methods for theming, I decided the best approach was stateful theming.

Stateful wha?

As the name implies, stateful theming means styling certain aspects of the UI based on a given state or condition. For example:

.t-red .title {
  color: red;
}

Where the usual way of going about it would be to add your theme class to your <body>:

<body class="t-red">
  <h1 class="title">A title</h1>
</body>

However, this method also allows you to have multiple co-existing themes along different contexts:

<body>
    <section id="post-1" class="t-red">
      <h1 class="title">Red title</h1>
    </section>
    
    <section id="post-2" class="t-blue">
      <h1 class="title">Blue title</h1>
    </section>
</body>

And while it’s not a particularly common case, this was exactly what I needed.

The Sassy Way

My initial approach involved SCSS, making use of the handy ampersand and maps. To give you an example, the code for buttons would look something like this:

.c-btn--primary {
  
  .t-red & {
    background-color: map-get($theme-red, primary);
  }
  
  .t-blue & {
    background-color: map-get($theme-blue, primary);
  }
}

.c-btn--secondary {
  
  .t-red & {
    background-color: map-get($theme-red, secondary);
  }
  
  .t-blue & {
    background-color: map-get($theme-blue, secondary);
  }
}

This approach seems to be fine if you have one or two additional themes apart from your default one. But in this case, I had five different color schemes. Every time I introduced a new themeable component, I had to manually add styles for each and every possible theme. And every time I wanted to create a new theme, I had to go through every variant of every themeable component, add a new theme class, and style it. Ugh.

Utility Classes

Stateful utility classes seemed like a decent solution to this problem. Instead of having to go one by one, I now had a couple utility classes that kept track of the current theme for each common property:

.u-color-primary {
  .t-red & {
    color: map-get($theme-red, primary);
  }
  
  .t-blue & {
    color: map-get($theme-blue, primary);
  }
}

This would eventually allow me to programmatically generate those utility classes with SASS, cutting down the time and effort required to introduce new themes significantly.

Custom properties to the rescue

But then I stumbled upon Harry’s article on theming with custom properties. And while I’m not necessarily following his approach, the article opened my eyes to the possibilities.

The main advantage of CSS Custom Properties over pre-processor variables is that they are available at runtime. That means you can change their values and see those changes reflected instantly on your site. CSS Custom Properties can also be declared multiple times and cascade, which is where the magic happens:

// Default theme
:root {
  --primary: #000;
  --secondary: #666;
}

// Blue theme
.t-blue {
  --primary: blue;
  --secondary: deepskyblue;
}

// Red theme
.t-red {
  --primary: red;
  --secondary: lightcoral;
}

Here, we have a default theme (defined in :root), and two additional UI themes, prefixed with the .t- namespace. If any of the values defined in a theme class is invalid, it will automatically fallback to the :root value.

Our markup will look exactly the same as before example, but our CSS will now become much cleaner:

.c-btn--primary {
  background-color: #000; // Fallback for unsupported browsers
  background-color: --var(primary);
}

.c-btn--secondary {
  background-color: #333; // Fallback for unsupported browsers
  background-color: --var(secondary);
}

With this approach, introducing new themes is now as simple as adding a new theme class and changing some values. Yay!

Although it’s cleaner, you don’t need to use :root to provide a fallback theme, as the var() function has support for specifying fallback values as well:

.c-btn--primary {
  background-color: #000; // Fallback for unsupported browsers
  background-color: --var(primary, #000); // var() with fallback
}

Whichever way you prefer, you should use a pre-processor to avoid repetition when providing fallback values.

Using the :root approach and SASS, you could so something like:

// Default Theme
$theme-default: (
  --primary: blue,
  --secondary: green,
);

// Programmatically generate fallback :root variables from the default theme
:root {
  @each $name, $value in $theme-default {
     #{$name}: #{$value};
  }
}

// Mixin
// 1. Grab the fallback value from the default theme
@mixin var($property, $variable) {
  #{$property}: map-get($theme-default, $variable); // [1]
  #{$property}: var($variable);
}

// Usage example
body {
  @include var(color, --secondary);
}

Final words

While CSS Custom Properties make tedious tasks like theming a bit easier, I will have to agree with Harry in that theming should be avoided unless there’s a real case for it, as it will increase code complexity, maintenance overhead and testing times.

At the time of writing this article, CSS Custom Properties have 67.21% global support. We also provide fallback to a default theme for browsers that don’t support them, so unless it’s vital for you to provide theming for IE11, Edge 14 or Opera Mini, Custom Properties are probably the way to go.

If you want to learn more about CSS Custom Properties, don’t miss Lea’s awesome presentation at CSSConf.

Join my newsletter and get articles like this delivered straight into your inbox.