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 better than the previous website!

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.

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

// get all the theme buttons from before
this.themeSelectBtns = document.querySelectorAll('button[data-theme-id]')
// 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))
})
}
}

// 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”).

OK that’s not actually a real term. I made that up.

<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 Rainbow Road theme.

Please don’t use that one.

Webmentions

What’s this?
  1. Atila.io 🧉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. ZanderZander
    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 KleinMichael Klein
    O wow, I really like it. The design of the entire site is top notch imo.
  5. Juha LiikalaJuha Liikala
    Best switcher I’ve seen to date! 😍
Show All Webmentions (66)
  1. Marc FilleulMarc Filleul
    Gorgeous
  2. WPbonsaiWPbonsai
    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 LuxHarald Lux
    Nice. BTW: on an iPhone the paintroller button is not really accessible below the menu button.
  4. Max BöckMax 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öckMax Böck
    Thanks! 🙌 feel free to copy anything you like, source is on github 😉
  7. Harald LuxHarald Lux
    Now it’s correct
  8. Max BöckMax Böck
    yeah most like the service worker; takes one extra session to update sometimes. thanks for checking!
  9. ChrisChris
    Love this!
  10. dickelippe @ homedickelippe @ 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 @ homedickelippe @ 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 NazehAr Nazeh
    I knew you will build that :D Super cool and great themes choice!
  13. Ar NazehAr Nazeh
    It keeps getting better!
  14. Max BöckMax Böck
    oh! that's likely an issue with focus-trap. I'll look into that - thanks for the hint! 🙌
  15. Anna MonusAnna Monus
    Best 🚀
  16. Maxime RichardMaxime Richard
    Awesome 👍
  17. Harry CresswellHarry Cresswell
    Max you’re on another level right now 👏
  18. Max BöckMax Böck
    Sure, feel free 😉
  19. Bridget StewartBridget Stewart
    Fact: All good design is derivative of Mario Kart. 💖
  20. Max BöckMax Böck
    It's just like... *A LOT* of colors 😅
  21. Stu RobsonStu 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öckMax Böck
    thanks for sharing, monica! 🙌
  24. Prince WilsonPrince 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öckMax Böck
    oooh like on carbon.now.sh - that would be nice!
  27. Omar LópezOmar López
    Color Theme Switcher | Max Böck - Frontend Web Developer mxb.dev/blog/color-the…
  28. Aditi AgarwalAditi Agarwal
    Instead of a light or dark theme switcher, this website provides a whole range of 10 dynamic color themes 😍 !
  29. Jason Mayo 🍩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 MamminoLuciano Mammino
    Color Theme Switcher by @mxbck mxb.dev/blog/color-the…
  33. JemJem
    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 BissetDavid 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. SpeckyboySpeckyboy
    Color Theme Switcher - Learn how to add multiple color schemes to your website via CSS mxb.dev/blog/color-the…
  36. SDS LabsSDS 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 HostingEco 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💫 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öckMax 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 🇿🇦Rob Hope 🇿🇦
    Ah nice one Timorthy - I didn't know the origin - thanks! Just edited the review with a credit to Max 👍
  42. Timothy MillerTimothy Miller
    Happy to help. Keep up the good work! 👍
  43. Rob Hope 🇿🇦Rob Hope 🇿🇦
    Timothy* sorry! Thanks again:)
  44. Max BöckMax Böck
    thanks! gotta admit I really like the movie theme in @brynjulfs1's version though 😉
  45. Håvard BobHå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. AgneyAgney
    Totally in love with this idea of color theme switcher mxb.dev/blog/color-the… via @mxbck
  47. Friday Front-EndFriday 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 CuratoloFabio Curatolo
    Excellent article to implement different color combinations on your sites mxb.dev/blog/color-the…
  49. tams sokaritams sokari
    Color Theme Switcher | Max Böck mxb.dev/blog/color-the…
  50. Daniel Bark 📦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 HawksworthPhil Hawksworth
    Discovered this lovely theme switcher while exploring @mxbck's beautiful web site. With added lobster. 🦞 mxb.dev/blog/color-the…
  52. Laura KalbagLaura 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öckMax 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 KarayiannisEmma Karayiannis
    😍😍😍😍
  55. Abdulla AlmuhairiAbdulla Almuhairi
    أتمنى لو أن المواقع توفر خيارات أكثر للألوان بدلاً من توفير الوضع المظلم فقط mxb.dev/blog/color-the…
  56. MayankMayank
    @zachleat @mxbck it's being actively worked on! (check the date 👀)https://tr.designtokens.org/format/ Design Tokens Format Module
  57. Tyler StickaTyler 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.