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:
- Identify the visual properties that need to change from one theme to the next.
- Come up with meaningful names for the properties identified. These will be the Semantic Variables.
- Configure each theme by choosing values for the semantic variables.
- Style UI components with the semantic variables.
- Apply the theme to the UI.
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
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-family
property 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 3Choose 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 4Style 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 5Apply 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