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.
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 example with some extra styling: codepen.io
Here's a link to the five color version on:
missingdice.com
Top comments (4)
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.
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.Great! Let me read an your tut about it 😉
Props to you for finding a no-js way of doing this. Consider me impressed.