Color Theming with CSS Custom Properties and Tailwind

Avatar of Michelle Barker
Michelle Barker on (Updated on )

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

Custom properties not only enable us to make our code more efficient, but allow us to work some real magic with CSS too. One area where they have huge potential is theming. At Atomic Smash we use Tailwind CSS, a utility class framework, for writing our styles. In this article, we’ll look at how custom properties can be used for theming, and how we can integrate them with Tailwind to maximize the reusability of our code. We won’t cover getting up and running with Tailwind — check out the official documentation for that — but even if you’re new to it you might find some of these tips useful.

Theming overview

Let’s say we have a “Call To Action” (CTA) component with a heading, body copy, and button.

A box with a light red heading that reads join our mailing list above a dark red body that reads be the first to hear about our new offerings right before a red signup button.

Writing regular (non-Tailwind) CSS for this color scheme would look something like this:

.cta {
  background-color: #742a2a; // dark red
  color: #ffffff; //white
}
    
.cta__heading {
  background-color: #e53e3e; // medium red
  color: #742a2a;
}


.cta__button {
  background-color: #e53e3e;
}

Using Tailwind, we would apply these colors as utility classes in our HTML:

<div class="bg-red-900 text-white">
  <h3 class="bg-red-600 text-red-900">Join our mailing list</h3>
  <div>
    <p>Be the first to hear about our new offerings</p>
    <button class="bg-red-600" type="button">Sign up</button>
  </div>
</div>

I’ve deliberately left out classes relating to anything other than the basic color scheme, but you can see the example in its entirety in this demo:

Now, if we wanted to apply a different color scheme to our component, we would need to override the color values of our original component. Without Tailwind, a common way to do that would be to append a theme class to the component itself, and redefine the color values lower down in the cascade. So for a component with a modifier class of .cta--blue (using the BEM convention) we’ll apply the CSS values for a blue color scheme:

.cta--blue {
  background-color: #2a4365; // dark blue
}


.cta--blue .cta__heading {
  background-color: #3182ce; // medium blue
  color: #2a4365;
}


.cta--blue .cta__button {
  background-color: #3182ce;
}
A box with a light blue heading that reads join our mailing list above a dark bluebody that reads be the first to hear about our new offerings right before a blue signup button.

If we’re using Sass or another preprocessor, it’s likely we’ll make life easier for ourselves by using variables for those color names, and we might nest the .cta__heading and .cta__body selectors. It doesn’t exactly make our code more concise, but it does make it more manageable by having a single place to update those values.

Now, suppose we have 10 different color schemes, as was my experience on a recent project. Our code starts to get longer, as we’re basically duplicating the above example 10 times in order to change those color values. Now imagine every component in our design system needs 10 color schemes, and many of those components are far more complex than our simple CTA. Maybe our themes need different fonts too. Suddenly we have a lot of CSS to write.

Theming with Tailwind

If we’re using Tailwind, on the other hand, we’d need to change multiple classes in the HTML itself. Even if we’re using a JavaScript framework, like React or Vue, this is not exactly a trivial task. In order to ensure unused styles are removed in a production build, Tailwind discourages the use of string concatenation for class names (at the time of writing). So building our themes means potentially piling a lot of logic into our components.

Theming with Custom Properties

By using custom properties for our color themes, we can drastically reduce the amount of code we need to write, and alleviate the maintenance burden. Let’s first take a look at how we can do this in regular CSS.

We define our custom properties as variables on the :root selector, making them global variables. (The body selector would serve us just as well.) Then we can use those variables in a selector, in place of our color property values:

:root {
  --primary: #742a2a; // dark red;
  --secondary: #e53e3e; // medium red
}


.cta {
  background-color: var(--primary);
  color: white;
}


.cta__heading {
  background-color: var(--secondary);
  color: var(--primary);
}


.cta__button {
  background-color: var(--secondary);
}

This is where the real magic happens: now the code for creating each of our themes becomes a case of only updating those custom property values. The new values will be inherited wherever we apply our theme class:

.th-blue {
  --primary: #2a4365; // dark blue
  --secondary: #3182ce; // medium blue
}

If we want a blue color scheme, we can apply that .th-blue class to the component, or even use it on the <body> tag to apply to apply a page-wide theme, which can be overridden on individual components as desired. Using a utility class potentially saves us writing even more code compared to a component-specific class (such as .cta--blue in the original code), as it could be applied anywhere in our codebase.

Handling older browsers

Like many agencies, plenty of our clients at Atomic Smash still require us to support Internet Explorer 11. While I’m okay with a progressive enhancement approach in most cases (by providing simpler fallback layouts for browsers that don’t support CSS Grid, for instance), I find theming is one area that often doesn’t allow for easy compromise. Clients want their brand colors and fonts seen, even on older browsers. Providing fallbacks using feature queries would entail a lot of extra work that would negate the benefits of using custom properties in the first place. To overcome this, we need a polyfill.

There are a couple of options for polyfilling custom properties in IE 11.

postcss-custom-properties

The first is using a PostCSS plugin called postcss-custom-properties. If you’re already using PostCSS in your workflow, this is fairly simple to add. It works by processing your CSS and outputting the result of the variable as the property value. So if you have the following CSS:

:root {
  --color: red;
}


h1 {
  color: var(--color);
}

The processed result will be:

h1 {
  color: red;
  color: var(--color);
}

Browsers that don’t support custom properties will ignore the second rule and fall back to the regular property value. There is also an option to remove the rules with the custom properties in the output, so the file size will be smaller. This means that no browsers will get the custom property — which is an issue if you’re updating variables dynamically — but you’ll be able to use them for static values in your code with no ill effects.

Unfortunately this polyfill has some limitations:

  1. You need to specify the file (or files) in your config where you’re defining the custom properties.
  2. Custom properties can only be defined on the :root selector.

The first limitation is relatively trivial, but the second unfortunately renders this polyfill entirely useless for our theming use case. It means we can’t redefine variables on a selector to create our themes.

ie11CustomProperties

This polyfill option involves serving a client-side script, rather than preprocessing the CSS. We can add the following script to our head to ensure the polyfill will only be loaded in IE 11:

<script>window.MSInputMethodContext && document.documentMode && document.write('<script src="https://cdn.jsdelivr.net/gh/nuxodin/[email protected]/ie11CustomProperties.min.js"><\/script>');</script>

This permits us to enjoy the full benefits of custom properties as in the examples here, so it’s the solution I decided to go with. It has a limitation where custom properties set in style attributes aren’t polyfilled. But I’ve tested it for the theming example above and it works just fine.

But what does this have to do with Tailwind?

As we’ve already seen, utility classes — single-purpose classes that can be applied anywhere in our HTML — can make our code more reusable. That’s the main selling point of Tailwind and other utility class frameworks — the size of the CSS file you ship should end up smaller as a result. Tailwind makes multiple color classes available: .bg-red-medium would give us a red background-color property value, .text-red-medium for color and so on for border, box-shadow, or any place you can think of that you might need a color value. 

Colors can be defined in a config file:

module.exports = {
  theme: {
    colors: {
      red: {
        medium: '#e53e3e',
        dark: '#742a2a'
      },
      blue: {
        medium: '#3182ce',
        dark: '#2a4365'
      }
    }
  }
}

If we want to use custom property values for our Tailwind classes, we can specify them in the config:

module.exports = {
  theme: {
    colors: {
      'th-primary': 'var(--primary)',
      'th-secondary': 'var(--secondary)'
    }
  }
}

I’m prefixing my colors and theme-related class names with th- so that it’s obvious they’re specifically related to theming, but feel free to use whatever convention suits you.

Now those classes will be available to us through Tailwind. Using .bg-th-primary gives us the equivalent of writing:

.some-element {
  background-color: var(--primary);
}

In our CSS we can define our custom properties for our themes as before:

:root {
  --primary: #742a2a;
  --secondary: #742a2a;
}


.th-blue {
  --primary: #2a4365;
  --secondary: #3182ce;
}

Let’s apply those classes to our HTML. The first example gives us a component with our default theme (the variables defined on the :root). The second has our blue theme. The only difference is the addition of the .th-blue class on the component. (Once again, I’ve omitted the classes unrelated to the theme, for brevity and clarity.)

<!--Component with default (red) theme-->
<div class="bg-th-primary">
  <h3 class="bg-th-secondary text-th-primary">Join our mailing list</h3>
  <div>
    <p>Be the first to hear about our new offerings</p>
    <button class="bg-th-secondary" type="button">Sign up</button>
  </div>
</div>


<!--Component with blue theme-->
<div class="th-blue bg-th-primary">
  <h3 class="bg-th-secondary text-th-primary">Join our mailing list</h3>
  <div>
    <p>Be the first to hear about our new offerings</p>
    <button class="bg-th-secondary" type="button">Sign up</button>
  </div>
</div>

Using the config as a style guide

Tailwind encourages you to define all variables in the config, and personally I agree that it’s a better approach. It means that the config file can be a single source of truth rather than (potentially) ending up with multiple places to define your colors and other theme values. Luckily, we can also use values from the Tailwind config file for our custom properties. We’ll need to first define all of our colors in the config (assuming we’re not using the default color palette included with Tailwind):

module.exports = {
  theme: {
    colors: {
      red: {
        medium: '#e53e3e',
        dark: '#742a2a'
      },
      blue: {
        medium: '#3182ce',
        dark: '#2a4365'
      },
      'th-primary': 'var(--primary)',
      'th-secondary': 'var(--secondary)'
    }
  }
}

Then we can access the theme object in the CSS:

:root {
  --primary: theme('colors.red.dark');
  --secondary: theme('colors.red.medium');
}


.th-blue {
  --primary: theme('colors.blue.dark');
  --secondary: theme('colors.blue.medium');
}

Wrapping up

I’m really excited about the benefits of being able to use custom properties without having to worry about browser support, even more so to be able to integrate them smoothly with our existing workflow. It’s hard to overstate the amount of time they will save us for theming. I hope that even if you’re not a Tailwind user, this article might encourage you to give custom properties a go for this use case.