DEV Community

Cover image for An HTML and CSS only multiple color scheme picker
Nathaniel
Nathaniel

Posted on • Originally published at endtimes.dev

An HTML and CSS only multiple color scheme picker

It's 2022 and lots of websites support dark mode. Around 7% of websites did in 2021

Many of these websites even have a toggle for changing between light and dark mode.

Dark mode is great, many people prefer it, it's easier on the eyes, and it extends battery life.

But what about other color schemes?

For my website missingdice.com I wanted to support more than two color schemes.

It's a website with tools for playing games — rolling dice, spinning wheels, that sort of thing — it's simple and it's for fun.

The site fulfils quite simple needs, so the same tool might be used for a kid's party, or by a group playing dungeons and dragons, or possibly some low stakes betting.

So i decided to include light, dark, party, vegas, and dungeons and dragons color schemes, and a high contrast mode — useful for some people with visual impairments, sunglasses, or if the screen is in direct sunlight.

I previously shared a tutorial on how to make a html and css only dark mode toggle — some found it useful and requested I publish a tutorial for the color-scheme picker. So here it is.

It covers much of the same ground as the dark mode toggle. It works fine without JavaScript, but also has some progressive enhancements for users with JavaScript enabled.

Here's what we're going to make…

A color scheme picker that…

  • works well without javascript
  • defaults to a user's dark mdoe preference
  • supports windows high-contrast mode and forced-colors

…as well as some progressive enhancements for users with JavaScript enabled…

  • Saves user preferences
  • Updates the <meta rel="theme"/> and <body> tag with the correct color scheme.

…and some graceful degradations for users on older browsers…

  • falls back to light mode if css variables (custom properties) aren't supported
  • remove the toggle if css variables aren't supported.

What kind of input to use

Since we have multiple values, and can only select one at a time — we have only two choices. Radio Buttons or a Select Box.

Due to our no-js limitation — we need to use an input who's state can be accessed in CSS.

Select box <option> elements are nested inside a <select> element — this makes their state inaccessible to the rest of the page using CSS.

So we're left with radio buttons — which is a good choice anyway!

How it works

To change our page's color scheme using just css — we need to make sure our radio buttons appear before everything else in our document.

We have a radio input for each of our color schemes. Then we can target the rest of our document with the :checked css selector.

if the radio input with id=blue is selected, we use the :checked css selector to target the rest of our document. The css selector looks like this: #blue:checked ~ *.

We can't style the <body> tag based on the radio inputs. So we create a <div> that our site's content goes inside, then style that <div> to fill the screen.

This causes a small issue when overscrolling and on non-rectangular screens, but we'll solve that later with some progressive enhancements.

We can then visually hide the <input> elements, and use their corresponding labels as buttons to toggle color scheme.

The labels can be placed anywhere on the page and will work fine so long as the label's for attribute matches it's corresponding radio input's id.

<body>


<input id="dark" class="color-scheme-button" name="color-scheme" type="radio"/>
<input id="light" class="color-scheme-button" name="color-scheme" type="radio"/>
<input id="blue" class="color-scheme-button" name="color-scheme" type="radio"/>

<!-- more radio buttons go here -->


<div class="color-scheme-wrapper">

    <label for="dark">dark mode</label>
    <label for="light">light mode</label>
    <label for="blue">blue mode</label>

    <!-- Site Content -->

</div>


<style>

.color-scheme-wrapper {
    min-height:100vh;
    background:white;
    color:black;
}


.color-scheme-wrapper {
    background:white;
    color:black;
}


#dark:checked ~ * {
    background:black;
    color:white;
}


#blue:checked ~ * {
    background:blue;
    color:white;
}

</style>

</body>

Enter fullscreen mode Exit fullscreen mode

This code works. But we still need to add a few more things. We'll start with CSS variables.

CSS variables (custom properties)

css variables save us a lot of clutter — without them we'd have to restyle every component on our website for each color scheme.

Instead we can define some variables and then change their values based on the selected color scheme.

For the sake of this tutorial we'll keep things simple — one color for the background and one for text.

First we create values for the default color scheme — this will be the light theme:


:root {

    --background:white;
    --text:black;

}

Enter fullscreen mode Exit fullscreen mode

Then we do the same for the other color schemes — this time applying the variables to every element that comes after the checked radio button that matches that scheme.

If a user checks a radio button, that color schemes' variables are applied to the whole site.


#dark:checked ~ * {

    --bg:black;
    --text:white;

}


#blue:checked ~ * {

    --bg:blue;
    --text:white;

}


.color-scheme-wrapper {

    background:var(--bg);
    color:var(--text);

}

Enter fullscreen mode Exit fullscreen mode

This works — but we need a fallback for browsers that don't support CSS variables — which as of writing is true for ~3.5% of web users.

css is very forgiving — if a browser comes accross a property it doesn't understand it will ignore it and keep going.

So, we can make our site look good on older browsers by repeating the property twice, once with a fallback color, and then again with our variable, like so:

.color-scheme-wrapper {

    background:white;
    background:var(--bg);

    color:black;
    color:var(--text);

}
Enter fullscreen mode Exit fullscreen mode

We can also hide our color scheme buttons for users who's browsers don't support them:

.color-scheme-button,
.color-scheme-button + label {

    display:none;

}

@supports(--css:custom-properties) {
    .color-scheme-button,
    .color-scheme-button + label {
        display:block;
    }
}
Enter fullscreen mode Exit fullscreen mode

Defaulting to user's preferred color scheme

Our current setup doesn't support preferred color scheme — meaning if a user who's has dark mode enabled on their device visits our site, they'll be met with our light theme.

Using the prefers-color-scheme css media query — we can make sure our visitor's preferred color scheme is the default.

We do this by "swapping" the variables of the light theme and dark theme if the user's device has dark-mode enabled — making the default color scheme "dark" and the dark theme "light".


:root {

    --bg:white;
    --text:black;

}

@media (prefers-color-scheme: dark) {
    :root {

        --bg:black;
        --text:white;

    }
}

#dark:checked ~ * {

    --bg:black;
    --text:white;

}

@media (prefers-color-scheme: dark) {

    #dark:checked ~ * {

        --bg:white;
        --text:black;

    }

}
Enter fullscreen mode Exit fullscreen mode

That's great, now if a user has dark mode enabled, the site will default to dark mode — however, if the user then wants to manually select light mode they need to select the radio button with the "Dark mode" label.

A small snag, but it has a simple fix.

We can use the same media queries to change the text in the <label> elements.

We create two <span> elements with the text "Light mode" and "Dark mode" in the labels — then create classes to display the relevant text based on the user's preferences.


<body>

    <div class="color-scheme-wrapper">

        <label for="light">
            <span class="dark-mode-hide">Light Mode</span>
            <span class="light-mode-hide">Dark Mode</span>
        </label>

        <label for="dark">
            <span class="dark-mode-hide">Dark Mode</span>
            <span class="light-mode-hide">Light Mode</span>
        </label>



    </div>

</body>



<style>

.light-mode-hide {
    display:none
}

@media (prefers-color-scheme: dark) {

    .light-mode-hide {
        display:inline
    }

    .dark-mode-hide {
        display:none
    }
}
</style>

Enter fullscreen mode Exit fullscreen mode

Now our label's change names to match.

Forced Colors and High Contrast Mode

For a variety of reasons, some users prefer specific color schemes to be applied to every web page they visit.

For these users selecting a color scheme on our website will have no effect.

Using css mediwe can remove the toggle for users with who have enabled either: forced colors or windows high contrast mode.

We can also use these css media queries to make other subtle changes to our design.

Windows High Contrast mode

-ms-high-contrast non-standard (microsoft only) css media feature. Introduced on Internet Explorer 11 on Windows 8 and continues to be supported on Edge.

@media (-ms-high-contrast: active) {

.color-scheme-button,
.color-scheme-button + label {

    display:none;

}

}
Enter fullscreen mode Exit fullscreen mode

Forced colors

Forced colors allows users to select their own color scheme for the web. It's often used to create high contrast color schemes, but can be used to make color schemes for other purposes too.


@media (forced-colors: active) {

    .color-scheme-button,
    .color-scheme-button + label {

        display:none;

    }
}

Enter fullscreen mode Exit fullscreen mode

Progressive Enhancements

Theme color meta tag

The theme-color meta tag attribute adds color to the browser's user interface (ui) — usually to the browser toolbar. See examples of theme-color in action here

It looks like this:

<meta name="theme-color" content="white" />
Enter fullscreen mode Exit fullscreen mode

You can also use media queries with these meta tags and have them match the user's preferred color scheme:

<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black">
Enter fullscreen mode Exit fullscreen mode

Styling the body tag

In some browsers the color of the <body> tag will be visible when the user overscrolls or if the screen has large rounded corners — like the new iphones and pixel phones. So we should also style the body tag with our default color schemes:

body {

    background:white;

}

@media (prefers-color-scheme: dark) {

    background:black;

}
Enter fullscreen mode Exit fullscreen mode

These are great for setting defaults — but when our users change the color scheme the theme-color meta tags and the body color will stay the same.

To fix this requires JavaScript — so we need to write some code to update the meta tags.

Enhancements with JavaScript

First we'll create an object with keys matching the ids of the radio buttons — the values are the colors we want to use to set the theme colors.


var themeColors = {

    dark: "black",
    light: "white",
    blue: "blue"

}

Enter fullscreen mode Exit fullscreen mode

We need to access the meta tags and the body in our javascript. To keep our code simple I've given the meta tags ids:

<meta id="light-theme-meta-tag" name="theme-color" media="(prefers-color-scheme: light)" content="white">
<meta id="dark-theme-meta-tag" name="theme-color" media="(prefers-color-scheme: dark)" content="black">
Enter fullscreen mode Exit fullscreen mode

Then we access them like so:

var lightThemeMetaTag = document.getElementById('light-theme-meta-tag')
var darkThemeMetaTag = document.getElementById('dark-theme-meta-tag')
var body = document.body
Enter fullscreen mode Exit fullscreen mode

Then we'll write a function that updates our meta tags and our body color. It takes user selected theme as a parameter.

We'll also add a variable selectedTheme to store the currently selected theme so it's accessible to other functions — we'll see why shortly


var selectedTheme = 'light'

function updateTheme(theme) {

    var color = themeColors[theme]

    lightThemeMetaTag.setAttribute("content", color);
    darkThemeMetaTag.setAttribute("content", color);
    body.style.backgroundColor = color;

    selectedTheme = theme

}

Enter fullscreen mode Exit fullscreen mode

Then we’ll listen for changes to our radio inputs.

If a change is detected, we’ll call the updateTheme function with id of the selected radio input.


var radios = document.querySelectorAll('input[type=radio][name="color-scheme"]');

radios.forEach(radio => radio.addEventListener('change', (event) => {

    var theme = event.target.id
    updateTheme(theme)

}));

Enter fullscreen mode Exit fullscreen mode

That updates our meta tags to match the currently selected theme.

There's one small problem though — on dark mode the values for our light and dark theme are the wrong way round!

So we’ll add some code that swaps the colors round if dark mode is enabled. It will also listens in the device's preferred color scheme and update accordingly. For instance, some people have their devices set to shift to dark mode at certain times.

First we'll write a function that swaps the light and dark colors around in our themeColors object.


function swapLightAndDark(){

    var light = themeColors.light
    var dark = themeColors.dark
    themeColors.dark = light
    themeColors.light = dark

}
Enter fullscreen mode Exit fullscreen mode

Then we add code the swaps the themes round if the device is in dark mode — and swaps them again if the user's device changes it's prefered color scheme.

Changes to the prefers-color-scheme variable also needs to call our setTheme function to trigger the changes. This is where the selectedTheme variable comes in handy.


var preferedColorScheme = window.matchMedia('(prefers-color-scheme: dark)');
var dark = preferedColorScheme.matches


if(dark){
    swapLightAndDark()
}

preferedColorScheme.addEventListener('change', () => {

    swapLightAndDark()
    setTheme(selectedTheme)

});




Enter fullscreen mode Exit fullscreen mode

Remembering user preferences

The biggest issue with not using JavaScript is that our user's preferences won't be remembered between visits — or even between pages on our site.

So, for users with JavaScript enabled we'll use local storage to store their preference.

First we'll write a function that saves the color scheme to local storage. It takes the current theme as a parameter:

function saveThemeToLocalStorage(theme){

    if(localStorage){
        localStorage.setItem('color-scheme', theme);
    }

}
Enter fullscreen mode Exit fullscreen mode

Then another function that checks if the user has a saved color scheme in local storage — if they do we'll update the theme and make sure we set that theme's radio input to checked.


function getThemeFromLocalStorage(){
    if(localStorage){

        var savedColorScheme = localStorage.getItem('color-scheme');

        if(savedColorScheme){
            var radioButton = document.getElementById(savedColorScheme);
            radioButton.checked = true;
            updateTheme(savedColorScheme)

        }

    }  
}
Enter fullscreen mode Exit fullscreen mode

Putting it all together

Then we can those function in our previously written code. Putting it all together looks like this:

var themeColors = {

    dark: "black",
    light: "white",
    blue: "blue"

}

var lightThemeMetaTag = document.getElementById('light-theme-meta-tag')
var darkThemeMetaTag = document.getElementById('dark-theme-meta-tag')
var body = document.body

var selectedTheme = 'light'


function updateTheme(theme) {

    var color = themeColors[theme]

    lightThemeMetaTag.setAttribute("content", color);
    darkThemeMetaTag.setAttribute("content", color);
    body.style.backgroundColor = color;

    selectedTheme = theme

    saveThemeToLocalStorage(theme)

}

function swapLightAndDark(){

    var light = themeColors.light
    var dark = themeColors.dark
    themeColors.dark = light
    themeColors.light = dark

}

function saveThemeToLocalStorage(theme){

    if(localStorage){
        localStorage.setItem('color-scheme', theme);
    }

}

function getThemeFromLocalStorage(){

    if(localStorage){

        var savedColorScheme = localStorage.getItem('color-scheme');

        if(savedColorScheme){
            var radioButton = document.getElementById(savedColorScheme);
            radioButton.checked = true;
            updateTheme(savedColorScheme)
        }
    }  
}


var preferedColorScheme = window.matchMedia('(prefers-color-scheme: dark)');
var dark = preferedColorScheme.matches


if(dark){
    swapLightAndDark()
}

preferedColorScheme.addEventListener('change', () => {
    swapLightAndDark()
    setTheme(selectedTheme)
});


var radios = document.querySelectorAll('input[type=radio][name="color-scheme"]');

radios.forEach(radio => radio.addEventListener('change', (event) => {

    var theme = event.target.id
    updateTheme(theme)

}));



// This must be called after the check for dark mode
getThemeFromLocalStorage()

Enter fullscreen mode Exit fullscreen mode

Finally, in order to make sure our theme is loaded from local storage before the page renders, we need to place this JavaScript in a tag after the radio inputs, but before the rest of our page.

Ideally this is inlined, to stop the page rendering being delayed by an http requrest.


<body>


<input id="dark" class="color-scheme-button" name="color-scheme" type="radio"/>
<input id="light" class="color-scheme-button" name="color-scheme" type="radio"/>
<input id="blue" class="color-scheme-button" name="color-scheme" type="radio"/>

<script>
    <!-- our color scheme script goes here -->
</script>

<div class="color-scheme-wrapper">

    <!-- Site Content -->

</div>

Enter fullscreen mode Exit fullscreen mode

That's it.

That's it!. With the above code and some styling you can make a very usable color scheme picker that works for as many people as possible.

I've created a demo in codepen. It uses all the code above, but with some extra styling, and a dropdown menu for selecting the color schemes, here it is:

Some final thoughts

Using a drop down for the color schemes

I explored a number of ways of displaying the color scheme options. But in the end I chose to use a drop down with a standard looking radio button for each option.

This allowed me to display the full labels for each color scheme without taking over the whole screen — as well as a clear label explaining what the buttons do.

I experimented with

  • showing a preview of the color scheme next to each option
  • styling buttons without text with the color of each theme
  • creating a multi-toggle switch looking thing
  • having a single button that cycles through each color theme.

I showed these to friends, and they mostly didn’t realize what they we’re for changing the color theme. Showing previes of the color scheme was also hard to decipher, especially if the theme was similar to the current theme.

So, a drop down with clear text labels was the winner.

Saving color schemes without JS!?

This color scheme picker was first created for missingdice.com. It's a project I work on when I don't feel like doing anything high stakes. It has lots of self-imposed rules about how it should work.

One of these rules is that every tool should work without JavaScript.

For instance, if a user has JavaScript disabled and they use the dice rolling tool. Instead of rolling dice on the client side, it submits a form to a server, which rolls the dice, and sends back a page with the result.

This works great! BUT, it means our lovely no-js color switcher effectively becomes useless. A user picks a color, then they submit the form, and their color preference is gone, the results of their roll are shown in the default mode.

So, how to get round this?

We place our entire page inside a <form>, and submit our users chosen color scheme along with the options for their dice roll! Then we make sure the server responds with results shown in their preferred color scheme.

I'll be adding that to the site soon, so stay tuned.

Top comments (0)