Design Systems, Pt. 5: Themes
After setting up our design tokens, custom properties, accessor functions and utility classes, let’s see how we can support multiple themes.
Overriding custom properties
We’ll assume that our themes only differ in the values of our design tokens.
Since our theme configuration is translated into custom properties, which we
defined on the :root
pseudo selector, it is easy to override them.
.theme-dark {
--color-background: #285453;
--color-text: #FFFFFF;
}
Then we can switch to the dark theme by setting the class on the html
element.
<html class="theme-dark">
<!-- ... -->
</html>
We can also set different custom properties for a single element.
.some-element {
--color-background: #63c7d4;
--color-text: #17545c;
}
And finally, we can get and set custom properties with JavaScript.
element = document.querySelector("#some-id");
getComputedStyle(element).getPropertyValue("--color-background");
element.style.setProperty("--color-background", "#285453");
Configuring themes with SASS
The approach above works fine, but there is one shortcoming: While the accessor functions that we defined ensure that we don’t use any variables that don’t exist, they are all based on the default theme. When we directly override the custom properties in our additional themes as described above, we don’t have any mechanism in place that saves us from setting custom properties that are not part of the theme.
Let’s see how we can improve this. We didn’t delve into the project structure yet, so we’re going to start with the part of the folder structure relevant for theming.
├── _functions.scss
├── _mixins.scss
├── main.scss
└── themes
├── _base.scss
├── _dark.scss
├── _default.scss
├── _functions.scss
└── _index.scss
In the root folder, we have the following files:
main.scss
: The entry point, which in the scope of this article only contains@use "themes";
._functions.scss
: Defines all the accessor functions from part 3, without the custom properties._mixins.scss
: Defines the mixin for defining custom properties from part 3.
Let’s have a closer look at the files in the themes
folder.
themes/_functions.scss
This file defines the generate-lines
, generate-sizes
and
number-to-fraction
functions from part 2. This module
is separate from the _functions.scss
in the root folder to avoid circular
dependencies.
themes/_base.scss
This file defines the design tokens as outlined in part 1. But instead of defining the $theme
variable
directly, we’re going to define a function that returns the theme.
For this blog, the file looks like this:
@use "sass:map";
@use "sass:math";
@use "sass:string";
@use "functions" as f;
$-base-line-height: 1.7411;
$-line-heights: (0.25, 0.5, 0.75, 1, 1.5, 2);
@function theme(
$text: #202c31,
$primary: #71819c,
$selection: #b15e5e,
$shade-primary: #a0a0a0,
$shade-secondary: #6b7380,
$light-primary: #eaeaea,
$light-secondary: #fafafa,
$light-tertiary: #b0b0b0,
$background: #fff
) {
@return (
colors: (
text: $text,
primary: $primary,
selection: $selection,
shade-primary: $shade-primary,
shade-secondary: $shade-secondary,
light-primary: $light-primary,
light-secondary: $light-secondary,
light-tertiary: $light-tertiary,
background: $background
),
font-families: (
main: "Lato, sans-serif",
alt: "Montserrat, sans-serif",
mono: string.unquote('"Source Code Pro", monospace')
),
base-size: 100%,
sizes: f.generate-sizes(1.149, 2, 6),
base-line-height: $-base-line-height,
line-heights: $-line-heights,
lines: f.generate-lines($-base-line-height, $-line-heights),
letter-spacings: (),
weights: (
normal: 400,
code: 500,
header-bold: 600,
bold: 700
),
radii: (),
spacer-base: 0.25rem,
border-widths: (
small: 1px,
medium: 3px,
large: 6px
),
gutter: 1.5rem,
measure: 75ch,
breakpoints: ()
);
}
The theme
function takes a bunch of optional arguments with default values.
This allows us to retrieve the default theme without passing any arguments, and
to override only the values that we care about.
The $-base-line-height
and $-line-heights
are private variables that are
needed both as values and as arguments for the generator functions. That’s the
only reason why they are defined separately.
The _functions.scss
from the root folder uses this base for the accessor functions:
@use "themes/base" as b;
@function theme($key) {
@return map.get(b.theme(), $key);
}
themes/_default.scss and themes/_dark.scss
These are the actual theme definitions. These modules call the theme
function
from themes/_base.scss
and use the define-custom-properties
to set the
custom properties.
The default
theme is just the base without any customizations. The custom
properties are defined directly on :root
.
@use "../mixins" as m;
@use "base" as b;
$theme: b.theme();
:root {
@include m.define-custom-properties($theme);
}
dark
is a theme with overrides, and you would have one of these files for
every additional theme. The custom properties are defined on a class with
the name .theme-<name>
.
@use "../mixins" as m;
@use "base" as b;
$theme: b.theme(
$background: #285453,
$text: #ffffff
);
.theme-dark {
@include m.define-custom-properties($theme);
}
themes/_index.scss
This is just the index file, and all it does is to import all the themes that you have.
@use "default";
@use "dark";
Summary
This approach still uses custom properties for theming, but adds a bit more safety and consistency.
One small drawback is that a complete set of custom properties is going to be defined for each theme, including properties that don’t differ from the default theme, and including properties that cannot be customized.
The example only allows to change the color values, but you can make more parts of the base configuration configurable, depending on your needs.
The one thing you cannot do with this approach is to add a whole different set of colors with other names, or to add additional font families or line height values etc. In general, all the map keys from the base are fixed; you can only override the values.
The theme files should only include what you saw in the examples. If you need to change the appearance of your components and layouts beyond choosing different property values, these styles should be put directly into your component and layout files.
One last thing to emphasize is the importance of semantic color names instead of
value-based color names for theming. If your design documents use names like
admin-green
, coral-blue
or shop-yellow
, you should first figure out the
semantics behind these colors.
Next part: Design Systems, Pt. 6: Variable Export