TL;DR; use these tips and get a dark mode in your web app. You can find a pull request adding the changes in this link Enjoy! 🎉
Dark mode and Web Accessibility: respecting the user's choice
We can see several pages with the dark-mode theme available for the users. This approach is more than design-only purposes but also covers usability and accessibility.
One of the most popular responses when you ask someone who is using dark-mode option on their web apps and/or their OS — operational systems — is that "it helps my eyes," or even "it’s elegant/neat/beautiful." However, Dark Mode developer documentation explicitly writes: "The choice of whether to enable light or dark appearance is an aesthetic one for most users, and might not relate to ambient lighting conditions."
Besides that, people with low vision can have some benefits having the option as well, preferring light text on a dark background by inverting the colors on the display, except for images, media, and some apps that use dark color styles. So that means your web app should support dark-mode if you're thinking and targeting accessibility.
It’s more than a choice, it’s about to build a web for everybody.
Dark mode via CSS media query
Thinking about the web as a powerful platform it is, we should expect an easy and simple integration. In this section, we'll dive into the prefers-color-scheme media query and how we can use CSS to support dark mode.
The prefers-color-scheme media feature can detect if the user has requested the page to use a light or dark color theme via operational system configuration. It accepts light or dark as values to notify the page if the current user chooses a page that has a light or dark theme.
One of the common approaches used to implement this feature is by adding a CSS Filter in your page. When you add the invert() CSS filter when it's dark and your background is white and your text is black, it inverts the colors.
Please check the browser support for CSS Filter based in your browser support list before use it
html { | |
background: #FFFFFF; | |
} | |
// Detect if the user has requested the system | |
// use a light or dark color theme. | |
// More details about `prefers-color-scheme` in https://web.dev/prefers-color-scheme/ | |
@media (prefers-color-scheme: "dark") { | |
html { | |
filter: invert(100%); | |
} | |
img, video { | |
filter: invert(100%); | |
} | |
} |
At the end, when the user system is with using dark-mode enabled, the page applies the rules added inside the media query content, so the background will be turned into black and the text will be white.
Dynamic Dark-Mode Javascript
Your pages can integrate dark-mode using Javascript as a combination to do so. It's important to understand that CSS and Javascript will use different memory partitions in your browser, so your users could have some benefits in performance and usability by combining both approaches.
Changing the theme by adding CSS classes
First of all, let's add a switch on the page to give the user the choice to toggle between light and dark modes. So when it's clicked, the page will be dark and by default, it will be using the light theme.
This is the HTML markup for the toggle that will be added in your page
<!-- Adding dark-mode switch toggle in your page --> | |
<div class="switch-toggle"> | |
<input type="checkbox" id="switch-checkbox" /> | |
<label for="switch" id="theme-switch" | |
>Enable/disable Dark Mode Theme</label | |
> | |
</div> |
Besides that, we need to apply some changes in our CSS. Now it also has classes to define if the current theme on the page is light or dark by adding light-mode and dark-mode classes and the styles for Switch Toggle Element into our HTML element. This will be the CSS with these changes.
/** Common CSS */ | |
html { | |
background: #FFFFFF; | |
} | |
html.dark-mode { | |
filter: invert(100%); | |
} | |
html.dark-mode img { | |
filter: invert(100%); | |
} | |
// Detect if the user has requested the system | |
// use a light or dark color theme. | |
// More details about `prefers-color-scheme` in https://web.dev/prefers-color-scheme/ | |
@media (prefers-color-scheme: dark) { | |
// Important | |
// you should add dark-mode if your html element | |
// does not have `.light-mode` class in place | |
// so you page can still respect the dark theme configuration | |
// from your user system | |
html:not(.light-mode) { | |
filter: invert(100%); | |
} | |
html:not(.light-mode) img { | |
filter: invert(100%); | |
} | |
} | |
/** Common CSS */ | |
/** Switch styles */ | |
.switch-toggle { | |
/** The display value will be changes via Javascript. | |
So it the browser supports media query manipulation, | |
it shows this option */ | |
display: none; | |
flex-direction: row; | |
margin-bottom: 45px; | |
margin-top: 20px; | |
min-height: 50px; | |
} | |
.switch-toggle input[type=checkbox] { | |
height: 0; | |
width: 0; | |
visibility: hidden; | |
} | |
.switch-toggle label { | |
cursor: pointer; | |
text-indent: -9999px; | |
width: 100px; | |
height: 50px; | |
background: #bada55; | |
display: block; | |
border-radius: 50px; | |
position: relative; | |
} | |
.switch-toggle label:after { | |
content: ""; | |
position: absolute; | |
top: 5px; | |
left: 5px; | |
width: 40px; | |
height: 40px; | |
background: #fff; | |
border-radius: 40px; | |
transition: 0.3s; | |
} | |
.switch-toggle input:checked + .active { | |
background: #bada55; | |
} | |
.switch-toggle input:checked + .active:after { | |
left: calc(100% - 5px); | |
transform: translateX(-100%); | |
} | |
.switch-toggle label:after { | |
width: 65px; | |
} | |
/** Switch styles */ |
After these changes, let's write the Javascript to manipulate and add the class to our page. Firstly, we should check if window.matchMedia is available on the user’s browser. If it is, we can start the javascript manipulation.
Secondly, it should check if dark mode is enabled via OS on page load, so it can add the dark-mode class and trigger the toggle on the switch. If it's supported we'll check if the HTML element already has any class (on page-load it won't have any class on it) so it adds or switches between light and dark mode classes and triggers a change on the switch to keep aligned with the current theme state.
Another good point in this step is that it should change the Switch Toggle content to be visible in case the browser supports it. Otherwise, it will be hidden via CSS.
This is the final state for the Javascript integration.
function toggleSiteTheme(e) { | |
var html = document.getElementsByTagName("html"); | |
var className = html[0].classList.value; | |
/** Gets the target by checking if it was triggered by | |
clicking on the switch or if via OS configuration changes */ | |
var target = document.getElementById("theme-switch"); | |
var isLightMode = target.className === "" || className === "light-mode"; | |
/** Adds the class into our HTML to determine if it will be light or dark mode */ | |
html[0].className = isLightMode ? "dark-mode" : "light-mode"; | |
/** Adds the class into our switch to show the user choices | |
for light or dark mode is active or not */ | |
target.className = isLightMode ? "active" : ""; | |
/** Checks the input and change the value of it. | |
So the CSS changes can be applies */ | |
document.getElementById("switch-checkbox").checked = isLightMode | |
? true | |
: false; | |
} | |
/** Firstly, we should check if window.matchMedia is available | |
on the user's browser */ | |
const isMatchMediaSupported = typeof window.matchMedia === "function"; | |
/** if it is, we can start the javascript manipulation */ | |
if (isMatchMediaSupported) { | |
const darkModeMatches = window.matchMedia("(prefers-color-scheme: dark)"); | |
/** Checks if dark mode is enabled via OS on page load, | |
so it can add the dark-mode class and trigger the | |
toggle on the switch */ | |
if (darkModeMatches.matches) { | |
toggleSiteTheme(); | |
} | |
/** Adding a listener for the switch toggle */ | |
document.getElementById("theme-switch") | |
.addEventListener("click", toggleSiteTheme); | |
/** If the browser supports it, we can change the switch content | |
to be visible. Otherwise, it will be hidden via CSS */ | |
document.getElementById("switch-checkbox").style.display = 'flex'; | |
} |
And this is the result of the switch to turn on and off your dark-mode in your page.
Listening for dark mode changes in OS
Now we can change slightly our javascript file to Adds a listener for dark mode changes via user system preferences. We can reuse the previous value for matchMedia method and add a listener to the changes.
function toggleSiteTheme(e) { | |
var html = document.getElementsByTagName("html"); | |
var className = html[0].classList.value; | |
/** Gets the target by checking if it was triggered by | |
clicking on the switch or if via OS configuration changes */ | |
var target = document.getElementById("theme-switch"); | |
var isLightMode = target.className === "" || className === "light-mode"; | |
/** Adds the class into our HTML to determine if it will be light or dark mode */ | |
html[0].className = isLightMode ? "dark-mode" : "light-mode"; | |
/** Adds the class into our switch to show the user choices | |
for light or dark mode is active or not */ | |
target.className = isLightMode ? "active" : ""; | |
/** Checks the input and change the value of it. | |
So the CSS changes can be applies */ | |
document.getElementById("switch-checkbox").checked = isLightMode | |
? true | |
: false; | |
} | |
/** Firstly, we should check if window.matchMedia is available | |
on the user's browser */ | |
const isMatchMediaSupported = typeof window.matchMedia === "function"; | |
/** if it is, we can start the javascript manipulation */ | |
if (isMatchMediaSupported) { | |
const darkModeMatches = window.matchMedia("(prefers-color-scheme: dark)"); | |
/** Checks if dark mode is enabled via OS on page load, | |
so it can add the dark-mode class and trigger the | |
toggle on the switch */ | |
if (darkModeMatches.matches) { | |
toggleSiteTheme(); | |
} | |
/** Adds a listener for dark-mode changes in OS */ | |
darkModeMatches.addListener((e) => toggleSiteTheme()); | |
/** Adding a listener for the switch toggle */ | |
document.getElementById("theme-switch") | |
.addEventListener("click", toggleSiteTheme); | |
/** If the browser supports it, we can change the switch content | |
to be visible. Otherwise, it will be hidden via CSS */ | |
document.getElementById("switch-checkbox").style.display = 'flex'; | |
} |
Adding better events
Another improvement you can apply is to use the better event listener for the current user device by using touchstart if is a mobile device. Otherwise, it will trigger based on the click event.
With all these changes, this will be the final version of the Javascript file.
function toggleSiteTheme(e) { | |
const html = document.getElementsByTagName("html"); | |
const className = html[0].classList.value; | |
/** Gets the target by checking if it was triggered by clicking on the switch or if via OS configuration changes */ | |
const target = | |
e && e.target ? e.target : document.getElementById("theme-switch"); | |
const isLightMode = target.className === "" || className === "light-mode"; | |
/** Adds the class into our HTML to determine if it will be light or dark mode */ | |
html[0].className = isLightMode ? "dark-mode" : "light-mode"; | |
/** Adds the class into our switch to show the user choices | |
for light or dark mode is active or not */ | |
target.className = isLightMode ? "active" : ""; | |
/** Checks the input and change the value of it. | |
So the CSS changes can be applies */ | |
document.getElementById("switch-checkbox").checked = isLightMode | |
? true | |
: false; | |
} | |
/** Firstly, we should check if window.matchMedia is available | |
on the user's browser */ | |
const isMatchMediaSupported = typeof window.matchMedia === "function"; | |
/** if it is, we can start the javascript manipulation */ | |
if (isMatchMediaSupported) { | |
const darkModeMatches = window.matchMedia("(prefers-color-scheme: dark)"); | |
/** Checks if dark mode is enabled via OS on page load, | |
so it can add the dark-mode class and trigger the | |
toggle on the switch */ | |
if (darkModeMatches.matches) { | |
toggleSiteTheme(); | |
} | |
/** Adds a listener for dark mode changes in OS */ | |
darkModeMatches.addListener((e) => toggleSiteTheme()); | |
/** If the browser supports it, we can change the switch content | |
to be visible. Otherwise, it will be hidden via CSS */ | |
document.getElementById("switch-checkbox").style.display = 'flex'; | |
} | |
/** Checks if it's a mobile device or not */ | |
const isMobileDevice = | |
typeof window.orientation !== "undefined" || | |
navigator.userAgent.indexOf("IEMobile") !== -1; | |
document.getElementById("theme-switch").addEventListener( | |
/** Using touchstart if is a mobile device. | |
Otherwise, it will trigger based on the click */ | |
isMobileDevice ? "touchstart" : "click", | |
toggleSiteTheme | |
); |
That’s all for now
It was great to write this blog post and see the evolution of the Web as Platform and how quick you can do amazing things like that — and even more if you like — in your pages.
If you like to know more about it, I'm applying this and other experiments on my personal website https://willmendesneto.github.io, or check the pull request adding this feature on the Github repository of my personal website.
Last, but not least, this is one of the various approaches that can be used to support dark-mode in your app. In the end, most important than the approach is to add it, keeping in mind that:
It can be Light or Dark mode, but more than this: let the users choose what’s best for them
I hope you enjoyed this reading as much as I enjoyed writing it. Thank you so much for reading until the end and see you soon!