Last week I was working on our new cockpit application, which is essentially a list of links to parts of our Wehkamp platform. The old application was not being maintained, as the React-stack is not something that's in the skill-set of most engineers. We kept the new cockpit simple: plain old HTML. Of course we wanted to support a nice dark-theme as well. This article shows how simple it is to implement dark mode.
Getting a light bulb
It all starts with a light bulb that can toggle the dark mode on and off . I love the Flat Icon site, which has a ton of light bulb icons. We went with this light bulb, created by Good Ware, and added it as an icon like this:
<a href="#" onclick="toggleDarkMode()" id="toggleDarkMode" title="Toggle dark mode">
<svg id="light" enable-background="new 0 0 24 24" height="20" viewBox="0 0 24 24" width="20" xmlns="http://www.w3.org/2000/svg"><g><path d="m13.5 24h-3c-.7 0-1.5-.6-1.5-1.8v-2.1c0-1-.5-1.9-1.3-2.6-1.8-1.4-2.7-3.4-2.7-5.6.1-3.8 3.2-6.8 6.9-6.9 1.9 0 3.7.7 5 2s2.1 3.1 2.1 5c0 2.1-.9 4.1-2.6 5.4-.9.7-1.4 1.8-1.4 2.8v2.3c0 .8-.7 1.5-1.5 1.5zm-1.5-18c-3.2 0-5.9 2.7-6 5.9 0 1.9.8 3.7 2.3 4.8 1.1.9 1.7 2.1 1.7 3.4v2.1c0 .2 0 .8.5.8h3c.3 0 .5-.2.5-.5v-2.3c0-1.3.7-2.7 1.8-3.6 1.4-1.1 2.2-2.8 2.2-4.6 0-1.6-.6-3.1-1.8-4.3-1.1-1.1-2.6-1.7-4.2-1.7z"/></g><g><path d="m14.5 21h-5c-.3 0-.5-.2-.5-.5s.2-.5.5-.5h5c.3 0 .5.2.5.5s-.2.5-.5.5z"/></g><g><path d="m12 3c-.3 0-.5-.2-.5-.5v-2c0-.3.2-.5.5-.5s.5.2.5.5v2c0 .3-.2.5-.5.5z"/></g><g><path d="m18.7 5.8c-.1 0-.3 0-.4-.1-.2-.2-.2-.5 0-.7l1.4-1.4c.2-.2.5-.2.7 0s.2.5 0 .7l-1.4 1.4s-.2.1-.3.1z"/></g><g><path d="m23.5 12.5h-2c-.3 0-.5-.2-.5-.5s.2-.5.5-.5h2c.3 0 .5.2.5.5s-.2.5-.5.5z"/></g><g><path d="m20.1 20.6c-.1 0-.3 0-.4-.1l-1.4-1.4c-.2-.2-.2-.5 0-.7s.5-.2.7 0l1.4 1.4c.2.2.2.5 0 .7 0 .1-.1.1-.3.1z"/></g><g><path d="m3.9 20.6c-.1 0-.3 0-.4-.1-.2-.2-.2-.5 0-.7l1.4-1.4c.2-.2.5-.2.7 0s.2.5 0 .7l-1.4 1.4c-.1.1-.2.1-.3.1z"/></g><g><path d="m2.5 12.5h-2c-.3 0-.5-.2-.5-.5s.2-.5.5-.5h2c.3 0 .5.2.5.5s-.2.5-.5.5z"/></g><g><path d="m5.3 5.8c-.1 0-.3 0-.4-.1l-1.4-1.5c-.2-.2-.2-.5 0-.7s.5-.2.7 0l1.4 1.4c.2.2.2.5 0 .7-.1.1-.2.2-.3.2z"/></g><g><path d="m16 12.5c-.3 0-.5-.2-.5-.5 0-1.9-1.6-3.5-3.5-3.5-.3 0-.5-.2-.5-.5s.2-.5.5-.5c2.5 0 4.5 2 4.5 4.5 0 .3-.2.5-.5.5z"/></g></svg>
</a>
We've added it as an SVG, so we can style it later on. Clicking the icon will trigger toggleDarkMode
.
Toggle dark mode
We're going to implement dark mode as a class on the html
element:
function toggleDarkMode() {
let dark = document.getElementsByTagName("html")[0].classList.toggle("dark");
document.cookie = "dark=" + dark;
}
Toggling dark mode is easy: just toggle the class. We also set cookie named dark
, to keep the mode dark active over sessions.
CSS with variables
Let's implement dark mode using CSS custom properties:
:root {
--header-shadow-color: rgba(85, 102, 119, 0.4);
--background-color: #fff;
--text-color: #000;
--text-hover-color: #5c5c5c;
--line-color: rgba(0, 0, 0, 0.1);
}
.dark:root {
--header-shadow-color: #999;
--background-color: #222;
--text-color: #aaa;
--line-color: #666;
}
It is that simple. Just add different colors when the dark
class is active. One advantage of custom properties is that a property can be reused. Here we use --text-color
for both color
and fill
(in case of an SVG):
a {
text-decoration: none;
color: var(--text-color);
display: inline-block;
}
#toggleDarkMode svg {
fill: var(--text-color);
}
a:hover {
color: var(--text-hover-color);
text-decoration: underline;
}
#toggleDarkMode:hover svg {
fill: var(--text-hover-color);
}
Apply dark mode
The last thing we need to do is apply the dark mode on reload. Let's support 3 use cases:
- The cookie
dark
is explicitly set totrue
orfalse
. - The query string parameter
dark
is explicitly set totrue
orfalse
(makes testing easier). - Or the user prefers dark theme with the media query prefers-color-scheme: dark.
(function () {
const getCookieValue = (name) =>
document.cookie.match("(^|;)\\s*" + name + "\\s*=\\s*([^;]+)")?.pop() || "";
const getQueryStringValue = (name) =>
new URLSearchParams(window.location.search).get(name);
// do as fast as possible to prevent white snap
// check explicit choice or user preference
const dark = getQueryStringValue("dark") || getCookieValue("dark");
if (
dark == "true" ||
(dark != "false" &&
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
document.getElementsByTagName("html")[0].classList.add("dark");
}
})();
Need something smaller?
!function(){const a=(a=>new URLSearchParams(window.location.search).get(a))("dark")||(a=>document.cookie.match("(^|;)\\s*"+a+"\\s*=\\s*([^;]+)")?.pop()||"")("dark");("true"==a||"false"!=a&&window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)&&document.getElementsByTagName("html")[0].classList.add("dark")}();
Make sure you load the script as soon as possible, preferably in the head
element.
Skipping Cloudflare Rocker Loader
Need to skip the Rocket Loader? Add the data-cfasync=false
to the script tag:
<script data-cfasync="false">
!function(){const a=(a=>new URLSearchParams(window.location.search).get(a))("dark")||(a=>document.cookie.match("(^|;)\\s*"+a+"\\s*=\\s*([^;]+)")?.pop()||"")("dark");("true"==a||"false"!=a&&window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)&&document.getElementsByTagName("html")[0].classList.add("dark")}();
</script>