Color Theme Switcher

Last year, the design gods decided that dark modes were the new hotness. "Light colors are for suckers", they laughed, drinking matcha tea on their fixie bikes or whatever.

And so every operating system, app and even some websites (mine included) suddenly had to come up with a dark mode. Fortunately though, this coincided nicely with widespread support for CSS custom properties and the introduction of a new prefers-color-scheme media query.

There’s lots of tutorials on how to build dark modes already, but why limit yourself to light and dark? Only a Sith deals in absolutes.

That’s why I decided to build a new feature on my site:
dynamic color themes! Yes, instead of two color schemes, I now have ten! That’s eight (8) better!

Go ahead and try it, hit that paintroller-button in the header.
I’ll wait.

If you’re reading this somewhere else, the effect would look something like this:

Nice, right? Let’s look at how to do that!

Define Color Schemes

First up, we need some data. We need to define our themes in a central location, so they’re easy to access and edit. My site uses Eleventy, which lets me create a simple JSON file for that purpose:

// themes.json
[
    {
        "id": "bowser",
        "name": "Bowser's Castle",
        "colors": {
            "primary": "#7f5af0",
            "secondary": "#2cb67d",
            "text": "#fffffe",
            "border": "#383a61",
            "background": "#16161a",
            "primaryOffset": "#e068fd",
            "textOffset": "#94a1b2",
            "backgroundOffset": "#29293e"
        }
    },
    {...}
]

Our color schemes are objects in an array, which is now available during build. Each theme gets a name, id and a couple of color definitions. The parts of a color scheme depend on your specific design; In my case, I assigned each theme eight properties.

It's a good idea to give these properties logical names instead of visual ones like "light" or "muted", as colors vary from theme to theme. I've also found it helpful to define a couple of "offset" colors - these are used to adjust another color on interactions like hover and such.

In addition to the “default” and “dark” themes I already had before, I created eight more themes this way. I used a couple of different sources for inspiration; the ones I liked best are Adobe Color and happyhues.

All my themes are named after Mario Kart 64 race tracks by the way, because why not.

Transform to Custom CSS Properties

To actually use our colors in CSS, we need them in a different format. Let’s create a stylesheet and make custom properties out of them. Using Eleventy’s template rendering, we can do that by generating a theme.css file from the data, looping over all themes. We’ll use a macro to output the color definitions for each.

I wrote this in Nunjucks, the templating engine of my choice - but you can do it in any other language as well.

/* theme.css.njk */
---
permalink: '/assets/css/theme.css'
excludeFromSitemap: true
---
/*
  this macro will transform the colors in the JSON data
  into custom properties to use in CSS.
*/
{% macro colorscheme(colors) %}
    --color-bg: {{ colors.background }};
    --color-bg-offset: {{ colors.backgroundOffset }};
    --color-text: {{ colors.text }};
    --color-text-offset: {{ colors.textOffset }};
    --color-border: {{ colors.border }};
    --color-primary: {{ colors.primary }};
    --color-primary-offset: {{ colors.primaryOffset }};
    --color-secondary: {{ colors.secondary }};
{% endmacro %}

/* 
  get the "default" light and dark color schemes
  to use if no other theme was selected
*/
{%- set default = themes|getTheme('default') -%}
{%- set dark = themes|getTheme('dark') -%}

/*
  the basic setup will just use the light scheme
*/
:root {
    {{ colorscheme(default.colors) }}
}
/*
  if the user has a system preference for dark schemes,
  we'll use the dark theme as default instead
*/
@media(prefers-color-scheme: dark) {
    :root {
        {{ colorscheme(dark.colors) }}
    }
}

/*
  finally, each theme is selectable through a 
  data-attribute on the document. E.g:
  <html data-theme="bowser">
*/
{% for theme in themes %}
[data-theme='{{ theme.id }}'] {
    {{ colorscheme(theme.colors) }}
}
{% endfor %}

Using colors on the website

Now for the tedious part - we need to go through all of the site’s styles and replace every color definition with the corresponding custom property. This is different for every site - but your code might look like this if it’s written in SCSS:

body {
    font-family: sans-serif;
    line-height: $line-height;
    color: $gray-dark;
}

Replace the static SCSS variable with the theme’s custom property:

body {
    font-family: sans-serif;
    line-height: $line-height;
    color: var(--color-text);
}

Attention: Custom Properties are supported in all modern browsers, but if you need to support IE11 or Opera Mini, be sure to provide a fallback.

It’s fine to mix static preprocessor variables and custom properties by the way - they do different things. Our line height is not going to change dynamically.

Now do this for every instance of color, background, border, fill … you get the idea. Told you it was gonna be tedious.

Building the Theme Switcher

If you made it this far, congratulations! Your website is now themeable (in theory). We still need a way for people to switch themes without manually editing the markup though, that’s not very user-friendly. We need some sort of UI component for this - a theme switcher.

Generating the Markup

The switcher structure is pretty straightforward: it’s essentially a list of buttons, one for each theme. When a button is pressed, we’ll switch colors. Let’s give the user an idea what to expect by showing the theme colors as little swatches on the button:

a row of buttons, showing the theme name and color swatches
Fact: All good design is derivative of Mario Kart

Here’s the template to generate that markup. Since custom properties are cascading, we can set the data-theme attribute on the individual buttons as well, to inherit the correct colors. The button also holds its id in a data-theme-id attribute, we will pick that up with Javascript later.

<ul class="themeswitcher">
{% for theme in themes %}
    <li class="themeswitcher__item">
        <button class="themepicker__btn js-themepicker-themeselect" data-theme="{{ theme.id }}" aria-label="select color theme '{{ theme.name }}'">
            <span class="themepicker__name">{{ theme.name }}</span>
            <span class="themepicker__palette">
                <span class="themepicker__swatch themepicker__swatch--primary"></span>
                <span class="themepicker__swatch themepicker__swatch--secondary"></span>
                <span class="themepicker__swatch themepicker__swatch--border"></span>
                <span class="themepicker__swatch themepicker__swatch--textoffset"></span>
                <span class="themepicker__swatch themepicker__swatch--text"></span>
            </span>
        </button>
    </li>
{% endfor %}
</ul>
.themepicker__swatch {
    display: inline-block;
    width: 1.5em;
    height: 1.5em;
    border-radius: 50%;
    box-shadow: 0 0 0 2px #ffffff;

    &--primary {
        background-color: var(--color-primary);
    }
    &--secondary {
        background-color: var(--color-secondary);
    }
    &--border {
        background-color: var(--color-border);
    }
    &--textoffset {
        background-color: var(--color-text-offset);
    }
    &--text {
        background-color: var(--color-text);
    }
}

There’s some more styling involved, but I’ll leave that out for brevity here. If you’re interested in the extended version, you can find all the code in my site’s github repo.

Setting the Theme

The last missing piece is some Javascript to handle the switcher functionality. This process is a bit more involved than we might initially assume. We need to check the user’s system preference through the prefers-color-scheme media query. But crucially, we also need to enable the user to override that preference, and then store the selected theme choice for later.

I’ve omitted some stuff here for brevity - see the full script on Github for all the details.

// let's make this a new class
class ThemeSwitcher {
    constructor() {
        // define some state variables
        this.activeTheme = 'default'

        // get all the theme buttons from before
        this.themeSelectBtns = document.querySelectorAll('button[data-theme-id]')
        this.init()
    }

    init() {
        // determine if there is a preferred theme
        const systemPreference = this.getSystemPreference()
        const storedPreference = this.getStoredPreference()

        // explicit choices overrule system defaults
        if (storedPreference) {
            this.activeTheme = storedPreference
        } else if (systemPreference) {
            this.activeTheme = systemPreference
        }

        // when clicked, get the theme id and pass it to a function
        Array.from(this.themeSelectBtns).forEach((btn) => {
            const id = btn.dataset.themeId
            btn.addEventListener('click', () => this.setTheme(id))
        })
    }

    getSystemPreference() {
        // check if the system default is set to darkmode
        if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
            return 'dark'
        }
        return false
    }

    getStoredPreference() {
        // check if the user has selected a theme before
        if (typeof Storage !== 'undefined') {
            return localStorage.getItem("theme")
        }
        return false
    }
}

// this whole thing only makes sense if custom properties are supported -
// so let's check for that before initializing our switcher.
if (window.CSS && CSS.supports('color', 'var(--fake-var)')) {
    new ThemeSwitcher()
}

When somebody switches themes, we’ll take the theme id and set is as the data-theme attribute on the document. That will trigger the corresponding selector in our theme.css file, and the chosen color scheme will be applied.

Since we want the theme to persist even when the user reloads the page or navigates away, we’ll save the selected id in localStorage.

setTheme(id) {
    // set the theme id on the <html> element...
    this.activeTheme = id
    document.documentElement.setAttribute('data-theme', id)

    // and save the selection in localStorage for later
    if (this.hasLocalStorage) {
        localStorage.setItem("theme", id)
    }
}

On a server-rendered site, we could store that piece of data in a cookie instead and apply the theme id to the html element before serving the page. Since we’re dealing with a static site here though, there is no server-side processing - so we have to do a small workaround.

We’ll retrieve the theme from localStorage in a tiny additional script in the head, right after the stylesheet is loaded. Contrary to the rest of the Javascript, we want this to execute as early as possible to avoid a FODT (“flash of default theme”).

👉 Update: Chris Coyier came up with the term “FART” (Flash of inAccurate ColoR Theme) for this, which of course is way better.

<head>
    <link rel="stylesheet" href="/assets/css/main.css">
    <script>
        // if there's a theme id in localstorage, use it on the <html>
        localStorage.getItem('theme') && 
        document.documentElement.setAttribute('data-theme', localStorage.getItem('theme'))
    </script>
</head>

If no stored theme is found, the site uses the default color scheme (either light or dark, depending on the users system preference).

Get creative

You can create any number of themes this way, and they’re not limited to flat colors either - with some extra effort you can have patterns, gradients or even GIFs in your design. Although just because you can doesn’t always mean you should, as is evidenced by my site’s new “Lobster Life” theme.

Please don’t use that one.

Webmentions

What’s this?
  1. Atila.io 🧉
    🤣 loved the theme names!!!! my favourite new one is Bowser's Castle!! Dark is still the overall winner for me, though. Really good palette! 🎨
  2. Zander
    Love this! Especially the names. I’m planning on adding something like this to my site 👍
  3. You're one of those people operating on Beyoncé time, aren't you? 😂 Extremely well done! …while I was netflixing the complete Fast&Furious series like a 00s faux column.
  4. Michael Klein
    O wow, I really like it. The design of the entire site is top notch imo.
  5. Juha Liikala
    Best switcher I’ve seen to date! 😍
Show All Webmentions (66)
  1. Marc Filleul
    Gorgeous
  2. WPbonsai
    wunderbar, but theme switcher is to big not fiting in your blog, I would make it smaller or position on the side like sidebar ;) ... but very nice
  3. Harald Lux
    Nice. BTW: on an iPhone the paintroller button is not really accessible below the menu button.
  4. Max Böck
    ah! could be an issue with service worker caching an outdated css file. would you mind sending me a screenshot?
  5. This is so nice! I’ve been working on multiple themes for my site, but wasn’t sure how best to toggle them. Your theme switcher is really neat! ✨
  6. Max Böck
    Thanks! 🙌 feel free to copy anything you like, source is on github 😉
  7. Harald Lux
    Now it’s correct
  8. Max Böck
    yeah most like the service worker; takes one extra session to update sometimes. thanks for checking!
  9. Chris
    Love this!
  10. dickelippe @ home
    Great idea, great color themes! I especially like the rainbow road, although I hated it back then... ;) One thing, though: When the theme switcher is opened, only links visible above the fold (ugh) work. The rest sends me to the top of the page, although URL is shown in statusbar
  11. dickelippe @ home
    Links that used to be below the fold and did not work, work fine after removing content and therefor pushing them above the fold. Tested on FF Dev Edition (76.0b8) on MacOS Mojave 10.14.6, no plugins installed.
  12. Ar Nazeh
    I knew you will build that :D Super cool and great themes choice!
  13. Ar Nazeh
    It keeps getting better!
  14. Max Böck
    oh! that's likely an issue with focus-trap. I'll look into that - thanks for the hint! 🙌
  15. Anna Monus
    Best 🚀
  16. Maxime Richard
    Awesome 👍
  17. Harry Cresswell
    Max you’re on another level right now 👏
  18. Max Böck
    Sure, feel free 😉
  19. Bridget Stewart
    Fact: All good design is derivative of Mario Kart. 💖
  20. Max Böck
    It's just like... *A LOT* of colors 😅
  21. Stu Robson
    This is neat! Color Theme Switcher from @mxbck mxb.dev/blog/color-the… Rainbow Road is 👍
  22. I enjoyed this fun read from @mxbck about how to add site support for multiple color themes 🎨. On my growing list of todos is refactoring my SCSS to be able to support themes... mxb.dev/blog/color-the…
  23. Max Böck
    thanks for sharing, monica! 🙌
  24. Prince Wilson
    I was thinking of adding something like this just for my code blocks and this just gave me more ideas 🤯
  25. 🤩 multiple syntax highlighting themes would be dope.
  26. Max Böck
    oooh like on carbon.now.sh - that would be nice!
  27. Omar López
    Color Theme Switcher | Max Böck - Frontend Web Developer mxb.dev/blog/color-the…
  28. Aditi Agarwal
    Instead of a light or dark theme switcher, this website provides a whole range of 10 dynamic color themes 😍 !
  29. Jason Mayo 🍩
    Nice colour switcher idea and implenation on @mxbck site. Uses Nunjucks, but easily convertible to #craftcms 👇 mxb.dev/blog/color-the…
  30. Переключатель цветовой темы. Макс Бёк собирает для своего блога различные темы и их переключатель на кастомных свойствах — mxb.dev/blog/color-the…
  31. 倪爽
    怎么在网页中实时切换配色主题? 自从暗黑模式流行之后…… #前端 Color Theme Switcher mxb.dev/blog/color-the…
  32. Luciano Mammino
    Color Theme Switcher by @mxbck mxb.dev/blog/color-the…
  33. Jem
    Holy shit, theme switchers from the early blogging scene are back in fashion. Who remembers these the first time round? mxb.dev/blog/color-the…
  34. David Bisset
    Interesting: @mxbck shares how he let users pick actual color schemes via theme switch on his site. #css #JavaScript mxb.dev/blog/color-the…
  35. Speckyboy
    Color Theme Switcher - Learn how to add multiple color schemes to your website via CSS mxb.dev/blog/color-the…
  36. SDS Labs
    Top story: Color Theme Switcher | Max Böck - Frontend Web Developer mxb.dev/blog/color-the…, see more tweetedtimes.com/justcreative/n…
  37. Eco Web Hosting
    Sure, you could have a "light mode" and a "dark mode" for your website. Or you could go galaxy brain like @mxbck and make a colour theme switcher to give people even more choice: mxb.dev/blog/color-the…
  38. 💫 Josh
    I forget if I've told you this before, or simply thought it to myself, but your site/blog is beautiful 😍 Also, agree, static sites are perfect for emergency sites like that. Can handle sudden unexpected traffic surges with no problems 💯
  39. Max Böck
    Oh thank you - likewise! 😅 Yeah it's amazing how much traffic a single server with static HTML can handle.
  40. Dennis Erdmann
    Let users customize your website with their favorite color scheme! Your site has a dark mode? That’s cute. Mine has ten different themes now, and they’re all named after Mario Kart race tracks. mxb.dev/blog/color-theme-switcher/
  41. Rob Hope 🇿🇦
    Ah nice one Timorthy - I didn't know the origin - thanks! Just edited the review with a credit to Max 👍
  42. Timothy Miller
    Happy to help. Keep up the good work! 👍
  43. Rob Hope 🇿🇦
    Timothy* sorry! Thanks again:)
  44. Max Böck
    thanks! gotta admit I really like the movie theme in @brynjulfs1's version though 😉
  45. Håvard Bob
    His site definitely was the inspiration for the themepicker👌 also a shoutout to @mackenziechild’s happyhues.co. Next step: neon Blade Runner theme like on codista.com (also Max’ work)
  46. Agney
    Totally in love with this idea of color theme switcher mxb.dev/blog/color-the… via @mxbck
  47. Friday Front-End
    Color Theme Switcher: "Let users customize your website with their favorite color scheme! Your site has a dark mode? That's cute. Mine has ten different themes now, and they're all named after Mario Kart race tracks." by @mxbck mxb.dev/blog/color-the…
  48. Fabio Curatolo
    Excellent article to implement different color combinations on your sites mxb.dev/blog/color-the…
  49. tams sokari
    Color Theme Switcher | Max Böck mxb.dev/blog/color-the…
  50. Daniel Bark 📦
    @mxbck I love your theme switcher! My favorite theme is Moo Moo Farm 🐄 mxb.dev/blog/color-the… #javascript #css #webdevelopment
  51. Phil Hawksworth
    Discovered this lovely theme switcher while exploring @mxbck's beautiful web site. With added lobster. 🦞 mxb.dev/blog/color-the…
  52. Laura Kalbag
    This is great, thank you! I’ve used a remarkably similar approach (using attribute selectors, similar semantics of variables) perhaps I’m just trying to do too much with too many variables, and that’s what’s making it so unwieldy.
  53. Max Böck
    looking forward to seeing it once you're done! 👍 finding a consistent "logic" was the biggest challenge for me too. My themes only have 5-6 variables and that was hard enough 😅
  54. Emma Karayiannis
    😍😍😍😍
  55. Abdulla Almuhairi
    أتمنى لو أن المواقع توفر خيارات أكثر للألوان بدلاً من توفير الوضع المظلم فقط mxb.dev/blog/color-the…
  56. Mayank
    @zachleat @mxbck it's being actively worked on! (check the date 👀)https://tr.designtokens.org/format/ Design Tokens Format Module
  57. Tyler Sticka
    @zachleat We used Theo until we ran into some limitations (and noticed its inventors seemed to have moved on). We transitioned to Style Dictionary after that. Not perfect but pretty flexible.