Mathias Polligkeit
  • Dev
  • Impro
  • Sheet Music
  • Contact
Nov 24, 2021 (last updated: Dec 7, 2021)

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

  • design-systems
  • sass

See Also

  • Design Systems, Pt. 9: Conclusion
  • Design Systems, Pt. 8: Layout Rules
  • Design Systems, Pt. 7: Project Structure
  • Design Systems, Pt. 6: Variable Export
  • Design Systems, Pt. 4: Generating Utility Classes
  • privacy policy