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.

Here's a CodePen with the final product (plus some styling)

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>

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>

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
}

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>

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);
}

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);
}

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>

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;
}
}

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 codePen example with some extra styling: codepen.io

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

published
14 Mar 2021
modified
14 Mar 2021
author
Nathaniel
tags
posts post html css a11y tutorial dark mode