Design

How to Design and Implemenent Theming

Theming in design systems

In the last post I talked about when and why it makes sense to use theming. But what exactly is a theme? How does it work? And what does it take to implement?

Style with semantic variables

The key to theming is styling UI components with semantically named variables (e.g. --color-text) rather than literal values (e.g. black).

.button-primary {
  /* color: black; */
  color: var(--color-text); 
}

Choose values for each theme

For each theme values are chosen for each of the semantic variables.

Semantic Variable Value (Theme A) Value (Theme B)
color-text #000000 #FFFFFF

Assign values to variables

When a theme is applied, the semantic variables are assigned the values for that theme, and that causes the look and feel of the UI to change. All this happens without having to go through the code to manually change individual styles.

Implementation

So an individual theme is a set of semantic variables and their corresponding values. But to implement theming requires more than that.

I think it can be broken down into five main parts or steps:

  1. Identify the visual properties that need to change from one theme to the next.
  2. Come up with meaningful names for the properties identified. These will be the Semantic Variables.
  3. Configure each theme by choosing values for the semantic variables.
  4. Style UI components with the semantic variables.
  5. Apply the theme to the UI.
Step 1

Identify Properties To Change

What not to change

Start by eliminating the things that shouldn't change from one theme to the next.

Themes shouldn't change the layout, relative hierarchy, or behavior of elements. So any properties that change those things should live with the component itself, not the theme.

For example, button sizes are controlled by the button component, not by the theme. Same with the relative hierarchy of elements. Headings are always going to be bigger than paragraph text.

It's not a perfect analogy, but you can think of the components (and their props) as the blueprints for the structure of a building and the theme is the paint that you apply to the walls. The paint doesn't change the size or layout of the rooms.

Factor out common properties

If you lay out every UI component and all their states and variants, plus all the different layouts and pages in an application, it might seem like there's an impossibly large number of properties that could change from one theme to the next.

You can't just list them all because you need the list of properties to be short and manageable, so that you can name them and remember them. And so that it's not too complicated and tedius to configure the theme and style with the semantic variables.

To get to a manageable number of properties, factor out common properties that are applied to multiple elements. For example, the same text color is applied in many places, so you'd factor it out into a common property that you can name and create a variable for.

But how do you identify common properties?

Go from general to specific

Rather than take a bottom up approach, going through every UI element and listing all the properties that might change, a better approach is to zoom out and think about styling more generally.

What categories of visual properties change the look and feel without changing layout, hierarchy or behavior? You can change:

  • Font family or typeface
  • Color
  • Shadow or elevation
  • Corner Radius
  • Border (style, thickness)

Next consider, where can these changes be applied?

  • Color can be applied to Text, Backgrounds, Borders, Icons/SVGs, and Shadows
  • Font family can be applied to Text only
  • Shadows, Borders, and Corner Radius can be applied to Interactive Elements or Containers

Finally, consider why are these changes applied and to what degree?

  • Conveys meaning (e.g. brand, success, warning, error)
  • Provides feedback on interactivity (e.g. hover, focus, active)
  • Creates levels of hierarchy by varying contrast (e.g. primary, secondary, tertiary)

By combining the what, where, and why of styling you can start to identify styles by use case.

For example, you can factor out a property for all instances where you:

  • Apply color
  • To text
  • To convey brand
  • Using the most prominent level of contrast
Step 2

Name Semantic Variables

The names need to describe what the variables are used for, hence semantic. They also need to be consistent and predictable.

Imagine you're the person styling a UI component. You want to be able to look at the property being styled and intuitively know what the corresponding variable name is.

.button-primary {
  background-color: var(--???);
  color: var(--???);
  border-color: var(--???);
}

Again, an approach that works well for naming variables is to start with the most general part of the property and work your way down to the most specific part.

What
Font
Color
Shadow
Radius
Border
Where
Text
Bg
Shadow
Radius
Border
Why
Neutral
Brand
Success
Caution
Error
Inverse
How Much
Primary
Secondary
Hover
High
Large

Chain together what + where + why + how much

To name a property, chain together modifiers from each column, what + where + why + how much. For example, the variable name for the most prominent text color would be:

--color-text-neutral-primary

What
Font
Color
Shadow
Radius
Border
Where
Text
Bg
Shadow
Radius
Border
Why
Neutral
Brand
Success
Caution
Error
Inverse
How Much
Primary
Secondary
Hover
High
Large

Cross out default variants

Now simplify the name by dropping any default values from the chain of modifiers.

The modifiers neutral and primary represent the most common, most used variant of the style. Only variants that differ from the defaults need to be explicitly specified.

So the variable name can be simplified from:

--color-text-neutral-primary

To just:

--color-text

What
Font
Color
Shadow
Radius
Border
Where
Text
Bg
Shadow
Radius
Border
Why
Neutral
Brand
Success
Caution
Error
Inverse
How Much
Primary
Secondary
Hover
High
Large

Whereas text with a brand color applied at the second most prominent level of contrast would be named:

--color-text-brand-secondary

Cross out redundant parts

Sometimes it's redundant to add specificity because all the information is already implied by the more general part of the name.

For example, consider a variable for an expressive typeface used for headings. Chaining together modifiers from the what + where + why + how much columns, you'd get:

--font-text-brand-primary

But the font-familyproperty can only be used on text. And the "how much" modifiers don't apply here either. So you can drop those parts of the name, leaving you with:

--font-brand

What
Font
Color
Shadow
Radius
Border
Where
Text
Bg
Shadow
Radius
Border
Why
Neutral
Brand
Success
Caution
Error
Inverse
How Much
Primary
Secondary
Hover
High
Large

Specialty names for unique elements

Style as many elements as possible with the general names defined above. They can usually cover the majority of cases. But sometimes certain UI elements are unique enough that they need variable names with even more specificity.

In these cases, you can add another modifier.

What
Font
Color
Shadow
Radius
Border
Where
Text
Bg
Shadow
Radius
Border
Element
Link
Nav Bar
Backdrop
Toolbar
Menu
Why
Neutral
Brand
Success
Caution
Error
Inverse
How Much
Primary
Secondary
Hover
High
Large

For example, the backdrop behind modals might have a unique background color used nowhere else in the UI. Chaining modifiers together to come up with a variable name, you'd get:

--color-bg-backdrop-neutral-primary

Which simplifies to:

--color-bg-backdrop

Or even just...

--color-backdrop

...since it's implied that the only color on a backdrop is the background color.

What
Font
Color
Shadow
Radius
Border
Where
Text
Bg
Shadow
Radius
Border
Element
Link
Nav Bar
Backdrop
Toolbar
Menu
Why
Neutral
Brand
Success
Caution
Error
Inverse
How Much
Primary
Secondary
Hover
High
Large

The reason to keep color in there is because there might be other properties that apply to the backdrop, like --blur-backdrop or --opacity-backdrop. It's best to keep the naming consistent, going from general to specific.

Create as few specialty variables as possible

One last callout here is to only create specialty names when the more general names don't work. The fewer variable names the easier it will be to choose values for and style with semantic variables. Try to strike a balance between the granularity of variation between themes and the ergonomics of the naming system.

Step 3

Choose Values for Semantic Variables

The values you choose determine the look and feel of each theme. The goal is to achieve the desired appearance while also ensuring the values work well together and serve specific purposes in the UI.

Constants instead of raw values

Instead of mapping semantic variables directly to raw values, like hex codes, it's better to choose from a predefined set of literal values that come from a design system.

These literal values are called Constants, and they're also just variables. But unlike semantic variables, constants are named in a way that literally describes the raw value they hold. Their names don't directly say where or when they're meant to be used.

For example, the raw value #1E1E1E might be named gray-950 since it's the darkest gray.

Semantic Variable Value (Theme A) Value (Theme B)
color-text gray-800 slate-800
color-text-secondary gray-600 slate-600
color-text-tertiary gray-400 slate-400
color-text-brand purple-700 blue-900

Scales and ramps

In a design system, constants are organized into scales or ramps, meaning values go from light to dark or small to large, in specific increments. The 950 in gray-950 indicates where it falls on the scale, and thefore how much of a specific attribute it has. In this case, darkness.

Every value on a scale has a job

The ends of a scale and the increments between values are purposefully chosen to work well together and serve specific purposes in the UI. Every value in a scale has at least one job its best suited for.

By choosing the right values for the job, you can:

  • Create relative hierarchy between elements (via size and contrast)
  • Create the contrast needed for accessibility
  • Make the UI feel cohesive and consistent
  • Provide feedback on interactive elements

Programatically choosing values

Since every value in a scale was chosen because its attributes make it well suited for a specific purpose, you can define rules for what scale-values map to which semantic variables.

For example, based on the attributes of contrast and darkness, color-text might always be the 800 value in a gray scale and color-border might always be the 200 value.

One optimization here is to write a script that automatically maps constants to semantic variables based on these rules. Not every semantic variable can be defined this way, but many can.

Using one source of truth

When mapping constants to semantic variables, you need a data structure (e.g. a table) that can hold this information and a place to store it.

Figma makes it easy to create tables of semantic variables and their corresponsing values. This is great for use in design files, but it's not ideal for use in code for a few reasons:

  • It's not easy for developers to access Figma variables to use in their code
  • Manually creating variables in Figma can be tedious and time consuming
  • Variable definitions can easily get out of sync between the design file and the code

Another option is to create a JSON file or a JS file with an object that maps semantic variables to values for each theme. This file can then be used to generate the variables used in code and in Figma, using scripts and Figma plugins.

Step 4

Style with Semantic Variables

Now it's a matter of styling the UI with the semantic variables. If a property is going to change from one theme to the next, it has to be styled with a semantic variable.

Of course for this to work, developers have to use the correct semantic variable for each property.

/* Style UI components with the semantic variables */
.button-primary {
  background-color: var(--color-bg-brand); 
  color: var(--color-text-onbrand); 
  border-color: var(--color-border-brand); 
}

Design with semantic variables

If the same variables are used in both the design file and the code, then developers can reference the designs and grab the correct variable names from the design file. This is another reason why it's useful to have a single source of truth.

Step 5

Apply the Theme

Everything done up to now was design. This step is how the design actually gets applied to the UI. I'll briefly outline how to do this using CSS custom properties and HTML data attributes.

In the app's global css file, the Constants and Semantic Variables defined in the previous steps have to be declared and defined in a parent element that wraps all the child elments where the theme will be applied. The root or html element, for example.

Data attributes can be used to differente the variable definitions for each theme in the code.

:root {
  /* Constants */
  --red-500: #FF0000;
}
html {
  /* Semantic Variables for Theme A */
  & [data-theme="theme-a"] {
    --color-bg-brand: var(--purple-700);
    --color-text-brand: var(--purple-700);
    --color-border-brand: var(--purple-700);
  }
  /* Semantic Variables for Theme B */
  & [data-theme="theme-b"] {
    --color-bg-brand: var(--blue-500);
    --color-text-brand: var(--blue-500);
    --color-border-brand: var(--blue-500);
  }
}

Finally, the theme is applied by adding a data attribute to the parent element where the variables are defined.

<html data-theme="theme-a">
  <button class="button-primary">Button</button>
</html>

Up Next

In the rest of this series, I'll go over how to design and implement theming in a design system. I plan to cover the following topics:

  • Palettes and Color Schemes
  • Typography
  • Other Styles (Shadows, Borders, Corner Radius, etc.)
  • Configuring and Generating Themes
  • Deciding What To Theme

Previous

Resources

A lot of the ideas and concepts in this post are based on what I learned from these resources:

  • Figma
  • Shopify Polaris
  • Radix
  • Tailwind CSS
More Writing
  1. Why Implement Theming?

    Theming in design systems

  2. 2023 Year In Review

    Reflecting on what happened in 2023

  3. How I’ve Been Using AI

    A look back at how I used AI in 2023

  4. Interactive Grid

    Playing with grid size, color, masking, and opacity