Blog Posts

Your CSS as source of truth for color themes

Building a themes switcher that parses your style sheet to find themes

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:

.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 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.

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โ„ข:

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:

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:

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 โฌ‡๏ธ

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 is appealing to you!