DEV Community

Cover image for An HTML and CSS only dark‑mode toggle button.
Nathaniel
Nathaniel

Posted on • Edited on • Originally published at endtimes.dev

An HTML and CSS only dark‑mode toggle button.

A quick guide to building an html and css only dark-mode toggle.

Using just css and html we'll build a button that:

  • changes between light-mode and dark-mode
  • defaults to the user's preferred color scheme
  • changes the label to reflect the user's preferred color scheme.

Why no javascript?

I've been building a website with tools for board game players. Simple things like dice and random playing cards.

One of my goals is for every tool to work without javascript. The site also has a dark-mode and light-mode. (and some other color schemes too).

I needed a way to toggle dark-mode without javascript — while still defaulting to the visitor preferred-color-scheme.

Here's my solution, simplified for this tutorial:

How it works:

Most dark-mode toggle buttons work by changing an attribute on the <body> tag, and then targeting that attribute in the css. Like so:

<body class="dark-mode">
    <!-- Site Content -->
</body>

<style>

body {
    background:white
}

body.dark-mode {
    background:black
}
</style>

<script>
 function toggleDarkMode() {
    // some logic to change the class on the body tag
 }
</script>
Enter fullscreen mode Exit fullscreen mode

This is very simple, but requires javascript to add and remove the dark-mode class.

Luckily we can still make changes to our styles without javascript. We can use CSS to target non-javascript user interactions.

Here we're going to use a checkbox, and the :checked pseudo-selector:

<body>
    <input id="color-mode" type="checkbox" name="color-mode">
    <label for="color-mode">Dark Mode</label>

    <!-- Site Content -->
</body>
Enter fullscreen mode Exit fullscreen mode

We need to make sure the input is the first thing in our <body> so we can target everything after it in our CSS.


body {
    background:white
}

#dark-mode:checked ~ * {
    background:black
}
Enter fullscreen mode Exit fullscreen mode

But there's a problem with this!

There's no way in CSS to target the parent of an element. So we can't change the color of the <body>.

So we'll use a work around. We'll place a <div> after our checkbox that does the job of the <body>. Then we style the <div> to fill the screen.

Now we can use the checkbox input to style our <div>:

<body>
    <input id="color-mode" type="checkbox" name="color-mode">
    <label for="color-mode">Dark Mode</label>

    <div class="color-scheme-wrapper">
        <!-- Site Content -->
    </div>
</body>

<style>

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

#color-mode:checked ~ .color-scheme-wrapper {
    background:black;
    color:white;
}

</style>

Enter fullscreen mode Exit fullscreen mode

This works! But there's still a few things we need to fix:

  • We need to make it default to the user's preferred color scheme.
  • We should use css variables because it will make life easier.
  • We need to change the label to reflect the user's preferences.

First let's add the css variables.

CSS variables allow us to define colors that change based on the checkbox. We'll use just two colors one for the background and one for text:

:root {
 --bg:#F4F0EB;
 --text:#141414;
}

#dark-mode:checked ~ .color-scheme-wrapper {
    --bg:#333;
    --text:#fff;
}


.color-scheme-wrapper {
    background:var(--bg);
    color:var(--text);
}
Enter fullscreen mode Exit fullscreen mode

Now, when we check the checkbox the variables change, and those changes are reflected in the rest of or css.

Defaulting to our visitors' preferred color scheme.

Now let's make it so it defaults to user's preferences. To target user preferences we can use a @media query.

Based on the result of the prefers-color-scheme media query we'll swap our light-mode and dark-mode themes.

So if a user's device has dark-mode enabled it starts off dark:

:root {
    --bg:white;
    --text:black;  
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg:black;
    --text:white;
    }
}

#color-mode:checked ~ .color-scheme-wrapper {
    --bg:black;
    --text:white;
}

@media (prefers-color-scheme: dark) {
    #color-mode:checked ~ .color-scheme-wrapper {
        --bg:white;
        --text:black;
    }
}


.color-scheme-wrapper {
    min-height:100vh;
    background:var(--bg);
    color:var(--text);
}
Enter fullscreen mode Exit fullscreen mode

Changing the label based on user preferences.

Now that we've swapped dark-mode and light-mode we need to make sure the label for our checkbox reflects this.

It would be confusing if the label said dark-mode was on when the screen was bright white.

There's a quick fix for this too. First we add two sets of text in our <label> one for each user preference:

<input id="color-mode" type="checkbox" name="color-mode">
<label for="color-mode">
    <span class="dark-mode-hide">Dark Mode</span>
    <span class="light-mode-hide">Light Mode</span>
</label>
Enter fullscreen mode Exit fullscreen mode

Then we hide one of the labels depending on the mode.

This set of media queries allows us to target both light-mode, dark-mode, and browsers that don't support prefers-color-scheme:


.light-mode-hide {
    display:none;
}

@media (prefers-color-scheme: dark) {
    .dark-mode-hide {
        display:none;
    }
}

@media (prefers-color-scheme: dark) {
    .light-mode-hide {
        display:initial;
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it. Let me know what you think!

If you can think of a clever way of having the color scheme remain the same after you've navigated to a different page. Let me know.

Also - There's a good argument for using input[type=radio] instead of input[type=checkbox]. But the concept is easier illustrated with a checkbox.

Links:

Here's a link to the example with some extra styling: codepen.io

Here's a link to the five color version on:
missingdice.com

Top comments (4)

Collapse
 
aka_tamer profile image
Tamer

Congratulations on the article! I really like the solutions found, I will treasure them.
I also wanted to adopt this solution in my last project, unfortunately it is not applicable because it does not store the preference selected by the user.

Collapse
 
shadowfaxrodeo profile image
Nathaniel

Thanks, glad you like it.
For my purposes not storing the preference is fine, but I'd like to find a way of doing it anyway. Maybe with local storage as a progressive enhancement.

Collapse
 
aka_tamer profile image
Tamer

Great! Let me read an your tut about it 😉

Collapse
 
coolscratcher profile image
CoolScratcher

Props to you for finding a no-js way of doing this. Consider me impressed.