An HTML and CSS only dark‑mode toggle button.
Tags: html, css, a11y, javascript
Published: 2021-03-14 20:01:48

**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.

{% codepen https://codepen.io/shadowfaxrodeo/pen/JjbzNMz %}

## Why no javascript?

I've been building [a website with tools for board game players](https://missingdice.com/dice-roller/). 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:

```html
<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:

```html
<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`.

```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>`:

```html
<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:

```css
: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:

```css
: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:

```html
<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`:*

```css

.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]`](https://css-tricks.com/yes-or-no/). But the concept is easier illustrated with a checkbox.

Links:

Here's a link to the example with some extra styling: [codepen.io](https://codepen.io/shadowfaxrodeo/pen/JjbzNMz)

Here's a link to the **five** color version on:
[missingdice.com](https://missingdice.com/dice-roller/)