Your CSS as source of truth for color themes
Tags: javascript, css, webdev
Published: 2020-02-02 23:53:37

---
title: Your CSS as source of truth for color themes
published: true
description: Building a themes switcher that parses your style sheet to find themes
tags: javascript, css, webdev
---

Supporting multiple color themes for your site can make it more attractive and accessible to users. Every bit of customization you can add to their experience is something that is well appreciated. In this post, I'll show a small technique I used recently to easily manage multiple themes using a single style sheet and CSS custom properties.

For this example, I have multiple color themes defined like this in my CSS style sheet:

```css
.theme-white {
  --base: 0, 0%;
  --lightness: 100%;
  --color: #1098ad;
}

.theme-dark {
  --base: 210, 10%;
  --lightness: 23%;  
  --color: white;
}

.theme-red {
  --base: 0, 100%;
  --lightness: 32%;
  --color: white;
}

/* etc */
```

I'm using the [hue-saturation-lightness model](https://css-tricks.com/hsl-hsla-is-great-for-programmatic-color-control/) here for colors. In the `--base` custom property I'm defining the hue and saturation for the primary color of the theme. What's great about it is that once you define your base colors in terms of hue and saturation, you can add variations for accents and highlights just by adjusting the lightness percentage (i.e. the `--lightness` property). The `--color` custom property in my particular case is just the secondary color I'm using for text, for my use case the good old hexadecimal notation was fined.

```css
body {
  --bgColor: hsl(var(--base), var(--lightness));
  --darkerBg: hsl(var(--base), calc(var(--lightness) - 10%));
  background-color: var(--bgColor);
  color: var(--color);
}
```

The `--bgColor` and `--darkerBg` properties are just derived from the `--base` and `lightness` variables. I find that this technique eases consistency when thinking about themes.

We need to get a list of all the available themes that we can change between. One way to do it is to maintain an array where we manually specify all of our available themes. However, this would mean that once we add or remove themes on our style sheet we would also need to add it in our script.

An alternative I've found useful is to read the defined themes from our style sheet. This way, every time the defined themes change, we won't need to adapt anything from our script and it will just work™:

```js
function getAvailableThemes() {
  const mainStylesheet = Array.from(document.styleSheets).find(
    s => s.title === "main"
  )

  const rules = Array.from(mainStylesheet.rules)

  // All themes are class selectors that start with "theme-"
  return rules
    .filter(
      r =>
        Boolean(r.selectorText) &&
        r.selectorText.includes("theme-")
    )
    // get name without dot prefix on a capture group
    .map(r => r.selectorText.match(/\.([^\s]*)/)[1])
}

const themes = getAvailableThemes()
```

`getAvailableThemes` will give us an array like `["theme-white", "theme-dark", "theme-red", "theme-orange", "theme-blue", "theme-purple"]`.

> **🌮 Note:** It's also possible to use data attributes and retrieve those from JavaScript instead of class selectors if you'd like to.

To toggle between the themes, I use a button with the following handler attached to it:

```js
const themeToggle = document.getElementById("change-theme")

const themes = getAvailableThemes()

themeToggle.addEventListener("click", function toggleTheme() {
  const currentTheme =  Array.from(document.body.classList).find(val => themes.includes(val)) ||
    defaultTheme
  const idx = themes.indexOf(currentTheme)
  const nextIdx = (idx + 1) % themes.length
  document.body.className = themes[nextIdx]
})
```

We can determine the default theme to use with the help of the `prefers-color-scheme` CSS media feature:

```js 
const mediaQuery = matchMedia("(prefers-color-scheme: dark)")
let defaultTheme = mediaQuery.matches ? "theme-dark" : "theme-white"

mediaQuery.addEventListener("change", val => {
  defaultTheme = mediaQuery.matches ? "theme-dark" : "theme-white"
})
```

If the user has defined "dark mode" on their operating system settings, we will be using those unless they switch to another one!

The full example ⬇️

{% codepen https://codepen.io/goshi/pen/VwYJRoy %}

I added additional code to store the selection on `localStorage` so that we can retrieve it on repeated visits. Hopefully, the idea of parsing the content of the style sheet to have a [single source of truth](https://en.wikipedia.org/wiki/Single_source_of_truth) is appealing to you!