Simplified flow of tokens into themes. The tokens on the left generate both the default (blue) and configured (red) themes. If you understand this graphic, you can skip the story. 🙃

The themes are always changing, but the tokens stay the same

How to build a configurable theme from design tokens

Mineral UI is an open source design system, built by CA Technologies, for which I am a core contributor.

Design tokens, popularized by Lightning Design System and helpfully explained by Nathan Curtis, are a valuable asset to any organization, particularly large ones with many products/platforms to support. Think of each token as a relatively static value representing a design decision. In the image above, the heading’s color, the padding on the text input and button, and the space between the components can each be represented by a design token. Those decisions can then be stored in a generalized form, typically JSON or YAML, and distributed in whatever formats necessary — Sass variables, Javascript objects, Sketch palettes, etc.

Theming is a powerful concept that aids in applying component styles consistently. By making your theme configurable, you can maintain that consistency while allowing your components to be used in a wider range of applications. For example, each product using the design system could have its own theme to differentiate from one another while still clearly belonging to the same family. Material’s case studies do a fantastic job of illustrating the power and flexibility of theming.

Mineral UI has always had robust theming capabilities. We recently took on two related challenges: (1) extracting design tokens from our theme (the result) while building the theme from those tokens, and (2) making the theme more easily configurable. The combination of the two was unexpectedly complex.

Why did we make our theme more configurable?
Authors using Mineral UI have long had the ability to override every theme property as they wish. Our theme represents several concepts — semantic colors like danger, success, and warning, as well as a “main” theme color — each of which relates to ~20 theme properties. For an author to change, say, the “danger” color from “red” to “magenta”, they’d have to override all of those properties individually. We needed to provide a way to do so by simply generating a theme with a configuration like colors: { danger: 'magenta' }.

The nature of design tokens (relatively static), would seem to be at odds with configurable theming (definitively dynamic). But, with the help of a wonderful tool called theo, we’ll see how they don’t have to be.

💭 The takeaway

Before we dive into the code, let’s explain the core concept behind it first. Take another look at that banner image:

Same image that starts this story

A token like “backgroundColor_brand” and its value of “{!brand_light}” represents two pieces of information: the color, “brand”, and the variation of that color, “light”. In a design system, relative values, like the variation of the color, are generally a much more important design decision than the absolute values, like the color its based on. This is because the relative values are expressing a relationship to the other tokens and how they’re used, while the absolute values are (most of the time) merely expressing an opinion. By using a consistent naming schema in our tokens and aliases, we can apply the more important relative aspect of the token to an arbitrary absolute value, which is exactly what we need for theming.

In other words, we can translate “{!brand_light}” to both “lightblue” and “lightred”, depending on configuration, and then reference that value with (almost) the same property.


A note on tools

This walkthrough will produce a simplified version of the system we devised for Mineral UI. Its React components use glamorous for their (CSS-in-JS) styling and we generate the token files with theo. The concepts should apply to a variety of tech stacks, though. You could alternatively generate your tokens with style-dictionary and build the same functionality with any styling solution that supports logic and variables—Sass, PostCSS, JS + CSS Custom Properties, etc…


🗒️ The task

We’ll start with our tokens, expressed in JSON, which serves as our single source of truth.

This snippet uses a slightly simplified version of the theo spec for a token file in an attempt at clarity. The main bit to grasp is that a value like "{!brand_base}" references the alias "brand_base", which will then resolve to the value 'blue'.

From tokens.json, we’ll generate a tokens.js file, where all aliases have been resolved, for use within our library:

We can then use those tokens to create the default theme:

…and an author’s custom configured theme that looks like this:

1️⃣ Before configuring anything, we need defaults

Using theo in a build script, we can convert our JSON source tokens to a more useful format with resolved alias values, as a simple JavaScript object. We can use this file to generate the default values for our theme.

Let’s name our function that creates a theme createTheme.

We’re now generating our default theme, which is a good start, but the property names are slightly incorrect.

⚙️ Use static design tokens to generate dynamic theme properties

Or: Why we use “brand” in our token names, but “theme” in the theme.

Tokens are meant to be relatively static entities; they should only change when the design decision they represent also changes. Themes, on the other hand, represent a dynamic set of values, whose default values align to the tokens. To denote this distinction, we’ll call the main color “brand” in tokens, which only needs to represent a single set of values, and “theme” in the theme, which needs to represent a variety of values.

To generate the proper theme properties from tokens, we must translate “brand” to “theme”. Roughly:

Now our default theme has the correct properties. 😎 Let’s keep building on createTheme to generate a configurable theme.

🎛️ Decide on an API

createTheme will take an options parameter, colors, that accepts colors for theme and gray properties (the two color concepts represented by our tokens/theme).

So, to create the theme output above, we’d call createTheme like this:

const myTheme = createTheme({ theme: 'red', gray: 'slategray' });

💪 The bulk of the work

If you look at our source tokens JSON, you’ll see that our colors are organized in a “light”, “base”, “dark” schema. If we called createTheme with no argument (in other words, if we create the default theme), then backgroundColor_theme will represent the “light” value of our brand color (blue), backgroundColor_theme_inverted will represent the “dark” value of our brand color, and so on.

But as we’ve already learned, this isn’t always the case with themes. The “light” and “dark” aspects align, but that color can be whatever the user wants it to be. We need to come up with a way to keep the parts of the design decision represented by the token, the “light” and “dark”, while swapping out the the blue brand color for our configured color.

To do that, we can create a mapping of each token’s name with the color alias it references. Once again, we’ll use theo:

That format type, colorAliases.js, isn't built-in to theo. Creating and registering that custom format can be an exercise for the reader, but you can reference ours, if you’d like. The main concept is that theo helpfully exposes the originalValue of each token prop as you’re iterating over them, which gives you the raw text of the alias, "{!brand_light}", rather than value, which gives you the resolved output, "lightblue".

Running that theo configuration will generate a colorAliases.js file that looks something like:

This colorAliases file is where the magic happens. It’s where we connect the theme variable to the information represented by the token, in a way that allows us to swap out some of that information while not losing the rest.

Armed with that file, we can build upon our previous step:

Almost there! Our createTheme now supports configuring the main theme color. But we still have “gray” to consider.

☑️ One last detail

Our remaining token, “borderColor”, is a bit different from the rest, in that the “gray” bit of information isn’t in the token name—like “brand” is in the others— it’s in the alias. We can extend our function to handle this:

We did it! 🎉 ⭐

Our function now supports setting both a main theme color and a gray color, covering all of our tokens. We have an easily maintained single source of truth for those tokens, and we can use them in a way that lets us create a theme built upon them, rather than an inflexible, direct relationship. Well done. Take a break — you earned it — then read on for further ways we can use this concept.

📈 Taking it further

Your tokens and theme are likely to contain a lot more properties than our simple case. E.g., our theme has colors for “theme” and “gray”, as above, but also semantic colors, “danger”, “success”, and “warning”, as well as “black” and “white”. Extending this concept to support more colors isn’t too tricky, but do try to optimize for the default case so you’re not doing a bunch of unnecessary operations in createTheme at runtime. (Better yet, use createTheme at build time to generate a static theme.)

Your tokens are also likely to be based on a color palette a little more complicated than “light”, “base”, and “dark”. Mineral UI’s palette uses colors with 10 values, mapped to “color_10” — “color_100”, for example. (Read more about our palette.) The logic above can be modified accordingly (ours, for reference).

Tokens can represent a lot more than colors. Organizations are using tokens to represent all sorts of values. Sometimes you may need to do some unit conversion between tokens, meant to be used in general contexts, and the theme, meant to be used in a very specific context. You can use theo’s transforms or you can extend createTheme to handle this.

Maybe you noticed that the code above used CSS color names and not hex/rgb/hsl. That means we could only accept valid CSS color names in our colors parameter (and only those colors with corresponding “light” and “dark” names). This, too, can be accommodated. You can even go so far as accepting an arbitrary “ramp” for each color option.

🎁 Wrapping up

We believe this combination of tokens and configurable theming to be a powerful pattern, and we’re eager to learn more of its benefits. If you’ve seen a similar approach, have any questions, or ideas for improvement, we’d love to hear from you.

Thanks for reading!

Special thanks to: