Before we start I would like to preface this by saying this is an article about the programmatic side of dark mode. For design-cetric information, see this super useful and detailed article by @abhirajb → https://hackernoon.com/how-to-implement-dark-mode-5-essential-tips-to-remember
So you wanna add darkmode to your website because that’s what all the cool kids are doing. I was in the same position, then I did a bit of googling and now I’m the master so sit down, open your editor and listen to my words.
To know what the clients system preference for dark mode.
To know what the clients website preference for dark mode.
To listen to whether the clients system preference has changed.
To listen to whether the website preference has changed.
Here is my wicked cool website. I created an index.html in the root of an empty directory.
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="utf-8">
<title>DARKMODE - Rules</title>
<script src="darkmode.js"></script>
</head>
<body>
<h1>My awesome website</h1>
<p>This website is a website.</p>
<!-- This is the button which will toggle dark mode -->
<button id="darkmode-button">Darkmode</button>
</body>
</html>
As you can see, there is a button element. We will use this to toggle dark mode on or off. There is also a link to two external resources, a javascript file to hold the logic for darkmode and css where the style rules for darkmode live.
This is what it looks like now, without dark mode enabled
Now is time to build the logic for determining when darkmode is applied.
Let’s write a little logic table to make sure our code follows a sensible set of rules. I want the website preference to take precedence over the system preference when toggled and the system preference to take precedence over the website preference when that is toggled. But you can choose your own logic, this just makes sense in my warped brain. It also offers a nice recipe to write code from.
Logic ID |
Users Operating System Default |
Website preference |
Darkmode Outcome |
---|---|---|---|
1 |
Light |
Light or None |
Light |
2 |
Light |
Dark |
Dark |
3 |
Dark |
Dark or None |
Dark |
4 |
Dark |
Light |
Light |
5 |
Light |
Light (Toggled) |
Light |
6 |
Light |
Dark (Toggled) |
Dark |
7 |
Dark |
Dark (Toggled) |
Dark |
8 |
Dark |
Light (Toggled) |
Light |
9 |
Light (Toggled) |
Light |
Light |
10 |
Light (Toggled) |
Dark |
Light |
11 |
Dark (Toggled) |
Dark |
Dark |
12 |
Dark (Toggled) |
Light |
Dark |
Now, open up darkmode.js
and start typing…
// This checks to make sure the DOM has loaded properly before we start messing with it
document.addEventListener('DOMContentLoaded', function(event) {
// DM logic in here...
}
The rest of the code will be inside this event listener.
Selecting the toggle button and body elements…
// Select the body element
let body = document.querySelector('body')
// Select the dark mode button
let darkMode = document.querySelector('#darkmode-button');
It will allow us to test if the dark mode button has been clicked and attach a class to the body element.
These will make the page dark or light.
We will do two things in our functions. The first is add a “dark“ class to our body element. This will trigger the css classes to be applied to the page. Secondly, add our chosen preference to local storage in the browser to we can persist the dark mode preference across all pages on the website and also remember the preference for when the user returns.
// Helper Functions
const makeDark = () => {
body.classList.add("dark");
localStorage.setItem('darkMode', 'dark');
}
const makeLight = () => {
body.classList.remove("dark");
localStorage.setItem('darkMode', 'light');
}
Check on startup for system preferences & check for local browser preferences. This is dealing with Logic ID’s 1 through 4 on my logic table.
After the helper functions add the following code.
// 1-4
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light').matches && localStorage.darkMode !== 'dark') {
// System is light, website is light or no preference = Make light
makeLight();
}
else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light').matches && localStorage.darkMode === 'dark') {
// System is light, website is dark = Make dark
makeDark();
}
else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark').matches && localStorage.darkMode !== 'light') {
// System is dark, website is dark = Make dark
makeDark();
}
else {
// System must be dark, website must be light or no preference
makeLight();
}
Listen for user clicks and dealing with logic IDs 5-8, which basically says if the user changes the darkmode, do exactly what they want. Therefore we can disregard the system preferences.
// Add an event listener which checks for the button being pressed, do exactly what the button says
darkMode.addEventListener('click', function() {
if (localStorage.darkMode === 'light') {
makeDark();
} else {
makeLight();
}
});
This is where you could argue that user preference of the website should take precedence, but I think; if the user has not set a preference on the website and they have their system change dark mode preference based on the time of day, then the website should follow this.
Also if they decide the system needs to be a specific preference and change to it manually, then the website should respect that decision and the user can change the website preferences if they do not want it to match system preferences. You could also add another button or a toggle which deals with this idea as another user preference.
Rant over…
Dealing with system preference changes
// 9-12 Add an event listener which checks for system preference changes, do exactly what it says
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
if (event.matches) {
makeDark();
} else {
makeLight();
}
});
The final JS file…
// This checks to make sure the DOM had loaded properly before we start messing with it
document.addEventListener('DOMContentLoaded', function(event) {
// Select the body element
let body = document.querySelector('body')
// Select the dark mode button
let darkMode = document.querySelector('#darkmode-button');
// Helper Functions
const makeDark = () => {
body.classList.add("dark");
localStorage.setItem('darkMode', 'dark');
}
const makeLight = () => {
body.classList.remove("dark");
localStorage.setItem('darkMode', 'light');
}
// 1-4
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light').matches && localStorage.darkMode !== 'dark') {
// System is light, website is light or no preference = Make light
makeLight();
}
else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light').matches && localStorage.darkMode === 'dark') {
// System is light, website is dark = Make dark
makeDark();
}
else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark').matches && localStorage.darkMode !== 'light') {
// System is dark, website is dark = Make dark
makeDark();
}
else {
// System must be dark, website must be light or no preference
makeLight();
}
// 5-8 Add an event listener which checks for the button being pressed, do exactly what the button says
darkMode.addEventListener('click', function() {
if (localStorage.darkMode === 'light') {
makeDark();
} else {
makeLight();
}
});
// 9-12 Add an event listener which checks for system preference changes, do exactly what it says
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
if (event.matches) {
makeDark();
} else {
makeLight();
}
});
})
So far, all we have seen is the body tag toggle with a .dark
class and the local storage change.
Here, we are going into darkmode.css
, and adding darkmode specific styling. All I need for my site is two declarations and two selectors. Yours will likely be more complex. But remember all you need to do to change something in dark mode is write a declaration with body.dark preceding the selector. It is likely lots of elements in your page will have a similar style, so you can chain the selectors as I have done below…
/* Darkmode Styling */
body.dark,
body.dark button {
background-color: black;
color: white;
}