Detect, set and save Dark Mode
This post is more than 5 years old
The media query
The prefers-color-scheme
media query will tell us if the user has set a preferred colour scheme in their OS. Possible values are no-preference
, light
and dark
.
For the purposes of this demo we are only going to be looking for dark
, and taking anything else as the default (for both the OS setting and the design of our site).
The simplest dark mode
Detecting the preferred colour scheme is extremely straightforward using a media query:
body {
background-color: white;
color: black;
}
@media (prefers-color-scheme: dark) {
body {
background-color: black;
color: white;
}
}
This is the simplest possible dark mode implementation, setting alternative background and font colours based on the result of the media query. It can be further refined using CSS custom properties (variables):
:root {
--background: white;
--type: black;
}
@media (prefers-color-scheme: dark) {
:root {
--background: black;
--type: white;
}
}
body {
background-color: var(--background);
color: var(--type);
}
A more flexible approach
Using JavaScript to detect the preferred colour scheme then using alternative styling in CSS is an approach that makes it possible to defy the result of the media query and set the colour scheme programatically.
Prepare CSS
Still using CSS custom properties, we can redefine colours that need to change based on a class on the body:
:root {
--type: #383735;
--background: #f7f3e9;
--highlight: #dd6969;
--mid: #787671;
}
:root body.dark {
--type: #f7f3e9;
--background: #383735;
}
It's standard to set global variables inside :root
, and since this is just a selector like any other we can select body.dark
below. Toggling the class will change the colours assigned to the variables which are used in our styles.
We only redefine the type and background colours as our design has a highlight and mid-grey colour that works with either the light or dark colour scheme.
Set the scheme in JavaScript
Media Queries aren't just for CSS, they can be used in JavaScript with window.matchMedia
. With this, we can conditionally add a class to the body tag:
function setInitialScheme() {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.bodyclassList.add('dark');
}
}
Change the scheme manually
Now that we have control of the colour scheme in JavaScript, we can change it programatically. First, add buttons to make the change:
<button type="button" class="js--change-scheme" data-scheme="light"></button>
<button type="button" class="dark js--change-scheme" data-scheme="dark"></button>
- Using a
button
instead of any kind of other element with a click handler keeps your site more semantic and accessible. - Set the
type
attribute to 'button' - by default, all button elements with no type are 'submit'. - The class
dark
on the button used to set the dark colour scheme is only to style this button differently. It has nothing to do with setting the dark colour scheme. - The class
js--change-scheme
is used to select the buttons in JavaScript. Using a dedicated class for JS selectors makes it easier to see that this element has an action and avoids sharing a class name between styles and JS that have nothing to do with one another. - The
data-scheme
attribute determines which colour scheme this button should activate.
Now we have the buttons, we need to handle their clicks. First, write a function that toggles the class on the body tag to change the colour scheme:
function changeScheme(scheme) {
if (scheme === 'dark') {
classList.add('dark');
} else {
classList.remove('dark');
}
}
- This function takes an argument of
scheme
. This will be the value of the data attribute on each of our toggle buttons. - It checks if the value of
scheme
is 'dark', and if it is it adds thedark
class to the body. - For any other colour scheme (or lack of) it'll remove the class from the body.
Now to handle clicking the buttons to run this function:
const schemeToggle = document.querySelectorAll('.js--change-scheme');
schemeToggle.forEach(button => {
button.addEventListener('click', () => {
changeScheme(button.dataset.scheme);
});
});
- Select all elements with the
js--change-scheme
class. - For each selected element, add a click event listener.
- When clicked, call the
changeScheme
function and pass the value of thedata-scheme
attribute as an argument.
Remember the chosen scheme
What we have so far is pretty good - set the colour scheme based on the OS settings but allow an override. This can be improved by remembering the choice of scheme using localStorage
and applying that when the site loads.
First, set the preferred scheme in localStorage
when it's changed using the buttons by updating the changeScheme
function:
function changeScheme(scheme) {
if (scheme === 'dark') {
classList.add('dark');
localStorage.setItem('as-scheme', 'dark');
} else {
classList.remove('dark');
localStorage.setItem('as-scheme', 'light');
}
}
When the scheme is changed we set our local storage to the value of the selected scheme, either dark
or light
. We never actually check if the saved colour scheme is light
, but by setting that specifically now we have the option to use it at a later date.
Next, we can check if this local storage has a value when we set the initial scheme:
const storedScheme = localStorage.getItem('as-scheme');
function setInitialScheme() {
if (storedScheme) {
classList.add(storedScheme);
return;
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.bodyclassList.add('dark');
}
}
- Get the stored colour scheme with
localStorage.getItem()
using the same name we set when changing scheme. - Check if there is a stored colour scheme, if there is set the class on the body and return.
- Now, if no scheme has been set in localStorage, check the preferred colour scheme with the media query.
Doing it in this order sets a bit of a hierarchy for the scheme to use:
- The scheme specifically chosen for this site.
- The scheme preferred in the OS setting.
- The default scheme for the site.
The end result
Now we've put all of these parts together, we have a site that can handle dark mode if set in the OS settings but also allows the user to have what they want.
This solution is totally vanilla. No JS frameworks, libraries, CSS-in-JS or pre-processors are needed to get it working. You could use these things to make this a little easier to manage if you work on a large app and/or part of a team.
For instance, Styled Components includes ThemeProvider, which makes the contents of a JSON theme file avaialble to all styled components via props. Mainintaining a pair of themes (one light, one dark) and providing the correct one based on the media query/local storage would mean your CSS needs zero adjustment, and you could drop in as many themes as you like. All that's needed is that your theme file is very carefully defined.