Dark Mode is a design trend where the color scheme of a website is changed to a dark background with light-colored text and elements. It's also referred to as Night Mode or Dark Theme. The purpose of Dark Mode is to reduce eye strain in low light environments, conserve battery life on mobile devices, and create a sleek and modern aesthetic.
Many popular websites and applications now offer a Dark Mode option — here’s TailwindCSS:
If you’re a developer, you most likely already know how to toggle Dark Mode in Dev Tools (for those who don’t know, go to the “rendering” tab!):
If you want to toggle on Dark Mode for your operating system (and thus all apps supporting Dark Mode), go to System Settings. On a Mac, you‘ll find it under System Settings > Appearance:
There are a lot of tutorials out there already on how to implement dark mode for a webiste.
This tutorial will look into different approaches, including some rad new CSS-features, that’ll allow you to ditch JavaScript.
Ready? Let’s get started.
Dark Mode using System Colors
First, we’ll create a simple HTML-document with a headline:
<body>
<h1>Hello Darkness, My Old Friend</h1>
</body>
In a stylesheet, we’ll add:
body {
color-scheme: light dark;
}
This will tell the browser, that our document can accept both a light and a dark color-scheme
.
If you run this snippett in a browser, even with Dark Mode on, it will be … light.
Your browser UI will be dark, though.
That’s because the user-agent-stylesheet does not have any default colors set up.
We can fix that quickly, by using System Colors:
body {
background-color: Canvas;
color: CanvasText;
color-scheme: light dark;
}
Let’s check our snippet again:
Much better! Dark Mode for your entire website in 3 lines of CSS!
Let’s dive a bit more into System Colors. From the specs:
In general, the
<system-color>
keywords reflect default color choices made by the user, the browser, or the OS. They are typically used in the browser default stylesheet, for this reason.
Here’s a Light Mode-demo, showing the available System Colors in Safari:
If we switch to Dark Mode, some colors are changed completely (like Canvas
and CanvasText
as we’ve already encountered), while others are only slightly changed:
Using System Colors for Dark Mode is a bit of a simplified Dark Mode-experience.
Yes, it will work — but pure black and white is a bit boring.
We can spice it up using color-mix
in CSS. We can mix a bit of CanvasText
(black or white) into Canvas
(white or black) for the background-color
, and the opposite for color
:
body {
background-color: color-mix(in srgb, Canvas, CanvasText 2.5%);
color: color-mix(in srgb, CanvasText, Canvas 15%);
}
This results in a “softer” look:
Deducting saturation from a color, is a widely used way to make variations of colors in Dark Mode.
Using relative colors in CSS, we can do exactly that:
background: hsl(from ActiveText h calc(s - 30%) l);
Unfortunately, relative colors don’t work with System Colors in any browsers — yet!
NOTE: System Colors can be overwritten with forced colors (although used rarely) — so don’t rely too much on this technique.
Let’s move on to another technique, that’ll allow us to fine-control our Dark Mode colors.
Using a prefers-color-scheme
media-query
To specify specific colors for light and dark mode, I recommend using CSS Custom Properties, and then update these using a prefers-color-scheme
media-query.
With Light Mode as our default, let’s add the colors to a :where(body)
-section, to separate them from our regular body
-styles:
/* Properties */
:where(body) {
--background-color: #FFF;
--text-color: #222;
}
body {
background-color: var(--background-color);
color: var(--text-color);
}
Then, for Dark Mode, we’ll simply update these colors-properties:
@media (prefers-color-scheme: dark) {
:where(body) {
--background-color: hsl(228, 5%, 15%);
--text-color: hsl(228, 5%, 80%);
}
}
Let’s check it out:
But what if we want the users to choose which version of our website they want, indepently of the System Settings?
They might prefer a system set to Dark Mode, but our website in Light Mode.
Let’s create a toggler!
Creating a color-scheme toggler
Using JavaScript
If you go to a site like TailwindCSS, you’ll notice that a dark
-class is added to the html
-node when you select ”dark” from the color-scheme-toggler. This is done with JavaScript:
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
Open Props is using a similar method, but updating a data-theme
-attribute instead, and then defining properties in two blocks:
[data-theme=light] {
--nav-icon: var(--gray-7); /* etc */
}
[data-theme=dark] {
--nav-icon: var(--gray-5); /* etc */
}
Using CSS
Using some brand new CSS-techniques, we can create a toggler without JavaScript.
We’ll create a toggler with 3 states:
- Light (forced)
- Auto (system default, could be either light or dark)
- Dark (forced)
First, some basic markup:
<fieldset>
<label>
<input type="radio" name="color-scheme" id="color-scheme-light" value="0">
Light
</label>
<label>
<input type="radio" name="color-scheme" value="auto" checked>
Auto
</label>
<label>
<input type="radio" name="color-scheme" id="color-scheme-dark" value="1">
Dark
</label>
</fieldset>
After adding some basic styles (see the Codepen-demo below), it renders like this:
We’ll add a --darkmode
-property and container-type
to the html-element:
html {
--darkmode: 0;
container-name: root;
container-type: normal;
}
We’ll be using @container style()
-queries, so we need to set the node as a “container”.
Since we don’t want to observe inline-size
-changes, we just add the value normal
.
If the user picks a “forced” value, we’ll update --darkmode
:
html:has(#color-scheme-light:checked) { --darkmode: 0; }
html:has(#color-scheme-dark:checked) { --darkmode: 1; }
And finally, we’ll use a container style()
-query to check, if --darkmode
is set to 1
:
@container root style(--darkmode: 1) {
body {
--background-color: hsl(228, 5%, 15%);
--text-color: hsl(228, 5%, 80%);
}
}
NOTE:
@container style()
-queries only works behind-a-flag in Chrome at the moment, it’s early days, so do not use in production.
Now, after selecting “Dark”, our toggler (and page) looks like this:
Simple and effective!
Storing state
If we want to store the users choice, I’m afraid we need a bit of JavaScript!
First, let’s add an identifier to our <fieldset>
:
<fieldset id="colorScheme">
Then, in JavaScript:
const colorScheme = document.getElementById('colorScheme')
colorScheme.addEventListener('change', event => {
localStorage.setItem('color-scheme', event.target.value)
})
Now, all we have to do is setting the property to the localStorage
-value when the document has been loaded:
window.addEventListener("load", event => {
const scheme = localStorage.getItem('color-scheme') || 'auto'
if (scheme) {
document.documentElement.style.setProperty('--darkmode', scheme)
}
})
To select the correct mode in the toggler, add this to the if
-block:
const selected = [...colorScheme.elements].filter(element => element.value === scheme)
if (selected) selected[0].checked = true;
Demos
Toggler
System Colors
Photo-credit: Sora Shimazaki
Top comments (20)
What is the general consensus in the community about referencing elements like this? As opposed to:
Isn't it considered unsafe and error-prone? I don't really remember, it's rarely seen in the wild, perhaps for a reason.
When I write example-code for articles, I tend to omit selectors, because I want to focus on what the code does, rather than how to select a Node in the DOM.
I assume most of Dev.to's readers knows how to do that anyway, and might want to use their own selectors (class-based, attribute-based, something else?) instead of just copy/pasting the code.
But I could be wrong! So thank you for your feedback — I've added this to the article:
It's best practice to use
document.getElementById
over the global variable because defining a local variablecolorScheme
would take precedence. It's also annoying to debug these global variables because they are not explicitly defined. When searching for something likecolorScheme =
you won't find anything, and finding something likecolorScheme.addEventListener
tells you nothing about the type of variable. However, findingdocument.getElementById
tells you it's a DOM element.I vote that implicit code is dangerous code and explicit code is safe code and there is no grey area 🫣
(see updated feedback above)
Very cool.
I've used this scheme of system themed colours when programming Windows desktop apps.
I didn't know it existed in CSS though - thanks!
👍🏻
Very detailed post! Great share! @madsstoumann Framing these screenshots would've been chef's kiss. I've built a simple OSS tool to help with screenshots with ease. Give it a shot and let me know what you think.
github.com/ekqt/screenshot
Cool!
Love this ❤️, Thanks for sharing.
👍🏻
This is something I'm for, Thanks for sharing! 🚀
👍🏻
Amazing code snippet.. thanks for telling.
You just saved a lot of time ⏲️ 😌
👍🏻
I love it.
👍🏻
Great tips, this was exactly what I've been looking for! 👏
👍🏻
If statement missing here: