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
anddark-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