Notes on implementing dark mode (2024)

As you can see from the pretty new toggle at the top, I recently added dark mode to this site. I thought this was something that’d never happen because over a decade I’d built up an inescapable legacy of fundamentally unmaintainable CSS, but for over a year I’ve been slowly making headway refactoring everything to Tailwind, and with that finally done, the possibility of dark mode was unlocked.

The internet is awash with tutorials on how to implement dark mode, so I won’t cover the basics, but while those tutorials will get you to a rudimentary dark mode implementation, I found that every one I read lacked the refinements necessary to get to a great dark mode implementation, with many of the fine details easy to get wrong. Here I’ll cover the big ones that are commonly missing.

Some select code snippets are included below, but a more complete version should be found in this site’s repository. HTML including Tailwind styling, JavaScript, and a little custom non-Tailwind CSS.

Frontend basics for backend people

First, a couple core concepts that’ll be referenced below.

If you’re not a frontend programmer, then you should be aware of the existence of the prefers-color-scheme CSS media selector which lets a web page react to an OS-level light/dark mode setting:

@media (prefers-color-scheme: dark) { // dark mode styling here}

We’ll also be making use of local storage. Although both are superficially key/value stores, local storage differs from cookies in that it’s intended for use from a client’s browser itself compared to cookies which are server-side constructs.

The only way to implement a permanently lived light/dark mode setting that’s persistent forever and across computers is to use a cookie to reference a server-side account where its stored in a database. That’s not possible for this site because it doesn’t have a server-side implementation or database, but local storage the next best thing. It also has the added benefits of having no default expiration date (so unless a user manually clears data, a light/dark mode setting is sticky for a long time), and sends less personal information to the server, which is broadly a good thing.

Tri-state

By far the most common mistake in dark mode implementations is to make it a bi-state instead of tri-state setting. At first glance it might seem like the only two relevant states are light or dark, but there are actually three:

  • User has explicitly enabled dark mode.
  • User has explicitly enabled light mode.
  • User has enabled neither dark mode nor light mode. Fall back to the preference expressed by their OS in prefers-color-scheme.

This is implemented by way of a three state radio button that’s heavily styled to look like the toggle you see above (Tailwind classes have been removed for clarity, but see the toggle’s template for gritty details):

<input value="light" name="theme_toggle_state" type="radio" /><input value="auto" name="theme_toggle_state" type="radio" /><input value="dark" name="theme_toggle_state" type="radio" />

On input change, store the selected value to local storage and add the CSS class dark to the page’s HTML element so that Tailwind knows to style itself with the appropriate theme:

// Runs on initial page load. Add change listeners to light/dark// toggles that set a local storage key and trigger a theme change.window.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.theme_toggle input').forEach((toggle) => { toggle.addEventListener('change', (e) => { if (e.target.checked) { if (e.target.value == THEME_VALUE_AUTO) { localStorage.removeItem(LOCAL_STORAGE_KEY_THEME) } else { localStorage.setItem(LOCAL_STORAGE_KEY_THEME, e.target.value) } } setThemeFromLocalStorageOrMediaPreference() }, false) })})

Use of auto (no explicit light/dark preference) styles the page based on prefers-color-scheme:

// Sets light or dark mode based on a preference from local// storage, or if none is set there, sets based on preference// from the `prefers-color-scheme` CSS media selector.function setThemeFromLocalStorageOrMediaPreference() { const theme = localStorage.getItem(LOCAL_STORAGE_KEY_THEME) || THEME_VALUE_AUTO switch (theme) { case THEME_VALUE_AUTO: if (window.matchMedia('(prefers-color-scheme: dark)').matches) { setDocumentClasses(THEME_CLASS_DARK) } else if (window.matchMedia('(prefers-color-scheme: light)').matches) { setDocumentClasses(THEME_CLASS_LIGHT) } break case THEME_VALUE_DARK: setDocumentClasses(THEME_CLASS_DARK, THEME_CLASS_DARK_OVERRIDE) break case THEME_VALUE_LIGHT: setDocumentClasses(THEME_CLASS_LIGHT, THEME_CLASS_LIGHT_OVERRIDE) break } document.querySelectorAll(`.theme_toggle input[value='${theme}']`).forEach(function(toggle) { toggle.checked = true; })}

Avoiding theme flicker on load

The next most common mistake is page flicker on load. The flicker is caused by the page initially styling itself with its default theme (usually light mode), then noticing that a different theme should be set and reskinning itself, but not before there’s an observable flash.

A lot of sites have so much crap happening when they’re loading that a flicker is lost amongst a sea of jarring effects (e.g. UI elements jumping around as sizes are determined suboptimally late), but a tasteful dark mode implementation takes care to avoid it.

The key to doing so is to make sure that the theme is checked initially by JavaScript that’s run inline with the page’s body. Putting it in an external file <script src="..."> or in a listener like DOMContentLoaded is too late, and will cause a flicker.

Common convention is to use a <script> tag right before body close:

<body> <script> ... // script must run inline with the page being loaded setThemeFromLocalStorageOrMediaPreference() </script> ...</body>

(I originally had the <script> tag right before </body> close instead of at the top on <body> open because that’s a more conventional place to put JavaScript, but found that even that was enough to produce a noticeable flicker when loading over a slower connection.)

Theme changes in other tabs

If a user has multiple tabs open to your site and changes the theme in one of them, it should take effect immediately in all others.

Luckily, our use of local storage makes this trivially easy. JavaScript provides the storage event for when a local storage key changes. Hook into that, and this problem is solved with five lines of code:

// Listen for local storage changes on our theme key. This lets// one tab to be notified if the theme is changed in another,// and update itself accordingly.window.addEventListener("storage", (e) => { if (e.key == LOCAL_STORAGE_KEY_THEME) { setThemeFromLocalStorageOrMediaPreference() }})

A changed theme takes effect instantly. A user clicks back to another tab and the new theme is there. No page reload required.

Take care that along with the page’s theme, setThemeFromLocalStorageOrMediaPreference() also sets any light/dark toggles to the right place.

Theme changes from the OS

The page should respond to OS-level changes in theme, which is easy via the matchMedia() API:

// Watch for OS-level changes in preference for light/dark mode.// This will trigger for example if a user explicitly changes// their OS-level light/dark configuration, or on sunrise/sunset// if they have it set to automatic.window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { setThemeFromLocalStorageOrMediaPreference()})

If you’re on macOS and have appearance set to “Auto” for light/dark mode to change at sunrise and sunset, this code makes sure that a site restyles itself automatically at that time. It also responds if a user manually sets their OS-level appearance. This is another small detail that most people won’t even notice (manually reloading the page will also set the right theme), but a good one nonetheless, and takes mere seconds to get right.

Side note: As a frequent critic of JavaScript, I have to acknowledge just how good its browser APIs are at helping developers get things done. Local storage and media matchers are powerful, complicated features, and yet we can plug into them knowing practically nothing about the elaborate effort that went into their internal implementations, and with only a handful of lines of code. Excellent work.

Syntax highlighting with Shiki

A site’s syntax highlighting for code blocks likely involves elaborate styling, and while some themes might look okay in either light or dark, it’s even better if the code theme changes along with the rest of the site.

I’d gotten tired of Prism’s various quirks, and recently made the move over to Shiki. One of its many benefits is easy support for dual light/dark themes with minimal configuration:

// Shiki will add its own `<pre><code>`, so go to parent `<pre>`// and replace the entirety of its HTML.codeBlock.parentElement.outerHTML = await codeToHtml(code, { lang: language, themes: { dark: 'nord', light: 'rose-pine' } })
html.dark .shiki,html.dark .shiki span { background-color: var(--shiki-dark-bg) !important; color: var(--shiki-dark) !important; font-style: var(--shiki-dark-font-style) !important; font-weight: var(--shiki-dark-font-weight) !important; text-decoration: var(--shiki-dark-text-decoration) !important;}
Notes on implementing dark mode (2024)

FAQs

Why implement dark mode? ›

Users with visual impairments, light sensitivity, or other conditions may find dark mode easier on the eyes. Implementing it makes apps more inclusive and user-friendly. Beyond the technical and accessibility benefits, dark themes also look sleek and modern.

What are the benefits of using dark mode? ›

Many popular apps and operating systems have embraced dark mode, incorporating it into their user interface options due to its benefits like eye strain reduction and battery saving. Notably, major operating systems like Windows 10 and 11, macOS, iOS, and Android all offer system-wide dark modes.

How do you implement a dark theme? ›

You can use a mix of Force Dark and native implementation to cut down on the time needed to implement dark theme. Apps must opt in to Force Dark by setting android:forceDarkAllowed="true" in the activity's theme.

What are the cons of dark mode? ›

The Cons of Dark Mode

While dark mode generally enhances contrast, it can also create visibility issues for specific accessibility tools. For example, dark mode can prevent focus indicators from being visible for a user who navigates by keyboard only.

Why do we prefer dark mode? ›

Reducing eye strain is the most common reason users with normal vision mention for using dark mode. As one research participant put it, “My eyes have always been very sensitive to bright lights. So ideally, I use dark mode on everything I can.

Why is dark mode better for the environment? ›

Most of the time, yes. Dark mode uses less energy than a traditional white background and the impact can be significant. A Purdue study found that when using auto-brightness the energy saving for dark mode is between 3 and 9%. If screen brightness is set to 100% the savings can be as high as 47%.

What is the difference between dark mode and dark theme? ›

What is dark mode? Dark mode (or dark theme) is a setting offered on many smartphone models. Dark mode displays an inverted color scheme — light-colored text and icons on a dark background. Most mobile devices are defaulted to light mode, in which dark text is superimposed on a white or light background.

How do I manage the dark theme? ›

Manage Dark mode in Chrome
  1. At the bottom right of a New Tab page, select Customize Chrome .
  2. Under “Appearance,” select either: Light : Chrome will be in a light theme. Dark : Chrome will be in a dark theme. Device : Chrome will follow your device's theme.

Is dark mode better for your brain? ›

Dark mode reduces blue light exposure

In fact dark mode is also considered as 'night mode'. Lowering the device's brightness settings also helps to protect your eyes from blue light exposure. Blue light emitted from digital screens is known to inhibit melatonin secretion by the pineal gland in our brain.

Is the dark theme better for eyes? ›

Dark mode should make it easier for your eyes to adjust from your dimly lit surroundings to your phone screen, reducing screen glare. This may reduce eye strain and minimize eye fatigue. This is why car navigation systems and GPS devices switch to dark mode after sunset.

When not to use dark mode? ›

While dark themes may be better suited for the night, they aren't necessarily helping you read better or saving your eyes from digital strain, or even saving a lot of juice on your device. You may also want to avoid dark mode altogether if you start noticing eyesight issues or increased sensitivity to light.

Why do developers use dark mode? ›

Some programmers may experience better readability with dark mode, while others praise light mode for catering to people with conditions like dyslexia. Ultimately, though, the difference between dark mode and light mode comes down to mere preference.

Why is dark mode good for accessibility? ›

Dark mode can benefit users with visual impairments by enhancing the contrast between text and background, and this way also improves readability. It can also reduce eye fatigue and make it easier to read by reducing the contrast between the screen and surrounding environment.

Why dark mode is better for apps? ›

Enhanced Readability

White or light-colored text on a dark background often provides better contrast, improving text readability. This is especially advantageous for apps with a significant amount of textual content, such as articles, blogs, or messaging apps.

Why is dark mode a trend? ›

Impact on User Experience. Dark mode's primary advantage lies in its ability to reduce eye strain and fatigue, especially during extended periods of screen time. The subdued contrast between text and background in dark mode minimises glare, providing a comfortable reading experience.

Top Articles
Latest Posts
Article information

Author: Prof. Nancy Dach

Last Updated:

Views: 5710

Rating: 4.7 / 5 (57 voted)

Reviews: 88% of readers found this page helpful

Author information

Name: Prof. Nancy Dach

Birthday: 1993-08-23

Address: 569 Waelchi Ports, South Blainebury, LA 11589

Phone: +9958996486049

Job: Sales Manager

Hobby: Web surfing, Scuba diving, Mountaineering, Writing, Sailing, Dance, Blacksmithing

Introduction: My name is Prof. Nancy Dach, I am a lively, joyous, courageous, lovely, tender, charming, open person who loves writing and wants to share my knowledge and understanding with you.