Mockup of a browser in Dark Mode + a Light Mode override button (🌕) which the website on display takes into account. Try out the prototype below.
When implementing Dark Mode and ways to toggle it, developers currently need to duplicate code and roll their own toggle implementation.
What if, instead, browsers would take care of all that?
~
Table of Contents
- Dark Mode 101
- The problem with Dark Mode Toggles
- Code Duplication as a red flag
- A better way of toggling
- Inherited Settings
- Demo
- In Summary
- Spread the word
~
# Dark Mode 101
Thanks to Media Queries Level 5 we can implement Dark Mode using only CSS. By also using CSS Custom Properties in the mix, it becomes very manageable:
html {
color-scheme: light dark; /* This site supports both light and dark mode */
}
:root { /* Default Light Mode colors */
--primary-color: black;
--primary-background: white;
}
@media(prefers-color-scheme: dark) {
:root { /* Dark Mode colors */
--primary-color: white;
--primary-background: black;
}
}
body {
color: var(--primary-color);
background: var(--primary-background);
}
Depending on the System/OS Color Theme setting, the site will either use black text on a white background (Light Mode) or white text on a black background (Dark Mode).
~
# The problem with Dark Mode Toggles
To offer users an easy way to switch between Light and Dark Mode, a lot of developers are offering toggle buttons their website. Instead of having the user go to the System Preferences, they can switch between Light/Dark Mode without ever leaving the site.
Developers who ever did implement a Light/Dark Mode toggle, might have noticed these side-effects/quirks:
-
To make this work, developers need to rely on either the Checkbox Hack or JavaScript to alter something – mostly a class or attribute – on the document:
<select name="color-scheme-"> <option value="system">System</option> <option value="light">Forced Light</option> <option value="dark">Forced Dark</option> </select>
document.querySelector('color-scheme').addEventListener('change', (e) => { document.documentElement.setAttribute('data-force-color-mode', e.target.value); });
-
Because of the way it is implemented (see step 1), duplication of the CSS code is required:
:root, :root[data-force-color-mode="light"] { /* Default Light Mode colors + Forced Light Mode */ --primary-color: black; --primary-background: white; } /* Dark Color Scheme (System Preference) */ @media (prefers-color-scheme: dark) { :root { --primary-color: white; --primary-background: black; } } /* Dark Color Scheme (Override) */ :root[data-force-color-mode="dark"] { --primary-color: white; --primary-background: black; }
-
To persist the setting across pages and reloads, JavaScript is required:
- Write the selected value after changing it (Most likely a cookie or Local Storage)
- Read the value on page load, to make sure the proper override class is set on the document
-
You'll need some more JavaScript as well to have the page respond to changes of the System Preference.
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addListener(() => { // Make sure the dropdown is up-to-date based on mediaQuery.matches });
But then again: do you still respond if the override has been set?
Above that many implementations I have seen don't take the "System" value into account. By omitting this option, the sites will never be able to respond to the system preference again, as they always have an override applied.
~
# Code Duplication as a red flag
One the things that immediately stands out as a red flag is the Code Duplication part. It's been bothering me before, and it is still bothering me today. The other listed issues are also issues, but are somewhat acceptable.
Thinking of ways to solve the Code Duplication part, I see these ideas floating around:
-
If we had a
:media()
pseudo-class available (see this proposal by Lea Verou), they could query it inside:is()
:root:is(.dark, :media(prefers-color-scheme: dark)) { --primary-color: white; --primary-background: black; }
-
In similar fashion, if Media Queries could also be used as selectors (see this suggestion), you can also group the selectors together
@media (prefers-color-scheme: dark) p, .dark-mode p { --primary-color: white; --primary-background: black; }
(Sidenote: I’m not a fan of this suggestion to be honest)
-
If we were able to programmatically change the value for
prefers-color-scheme
, the duplicated CSS part could be omitted. Something like:document.querySelector('color-scheme').addEventListener('change', (e) => { window.setMedia('prefers-color-scheme', e.target.value); });
UPDATE: Also see this CSSWG issue
These solutions, however, do not tackle any of the other listed issues. What about the JS requirement to respond to toggle changes, persist the value, read the value on load, …?
~
# A better way of toggling
What if, instead of having developers try and implement their own Dark Mode Toggle with all its intricacies, the browser would offer this functionality to the user?
Think of a button that is part of the browser's UI that you could click. Something like the button Chrome DevTools already sports, but then part of the browser UI, available for all users
Video of the feature as it appears in Chrome DevTools
With such a button in place, the user can easily toggle the preference, and developers don't need to do anything to offer easy switching.
Above that it would allow users to have their OS set to Dark Mode, while reading websites in Light Mode (or vice versa)
UPDATE 2024.04.13 I have built a POC extension for Google Chrome that does the job. It relies on the experimental Web Preferences API to perform the override. See this post for details.
~
# Inherited Settings
The way I see it, the chosen Dark/Light value would be persisted on a per-site basis – i.e. altering it would store the chosen preference for only that site.
To not force users to set the value for every new site they visit, there would also be a setting at the browser level.
Combined, you'd get a value that inherits down from the system level down to the site level:
- Dark/Light Setting at the OS level
- Dark/Light Setting at the Browser level (default value: inherit from OS)
- Dark/Light Setting at the Site level (default value: inherit from Browser)
With these in place, users get the freedom to have the sites they visit use a different color scheme from their OS (if they want) and to also make exceptions for certain websites.
UPDATE: Also see this Chromium issue
~
# Demo
You can play around with the CodePen below to see how altering these settings at the various levels would propagate onto a dummy browser UI + two webpages:
See the Pen Browser Level Dark Mode Toggle by Bramus (@bramus) on CodePen.
Be sure to try out the values to OS:Light+Browser:Dark+Site:Light for bram.us
, and then switch tabs to other.site
:
- The site
bram.us
will be Light, due to the Site setting being Light - The site
other.site
will be Dark, due to the Browser setting being Dark and its specific Site setting being inherit
👏 Shout out to Firefox for shipping a Dark/Light Setting at the Browser level in their Firefox 100 release. They also have an extra option, where the chosen browser theme can decide whether to apply Light or Dark Mode.
Now let's hope we can also get a setting at the Site Level?
UPDATE 2024.04.13 I have built a POC extension for Google Chrome that allows you to override the value for prefers-color-scheme
on a per-origin basis. It relies on the experimental Web Preferences API to perform the override. See this post for details.
~
# In Summary
In summary, I think there should be easier ways to switch Dark Mode on a per-site basis. Ideally, switching/overriding should be a simple button (or the like) at the browser level. That way we can get rid of all those custom light/dark mode buttons, and the browsers can sync the preference values to all the devices a user uses.
Should authors want to roll their own custom button, then a way to programmatically change the prefers-color-scheme
value from within JavaScript would be a welcome addition (csswg-drafts/issues#6517). That way the duplicated CSS code block can be removed and there would be less JS involved to achieve the act of switching:
window.setMedia('prefers-color-scheme', 'dark');
In this scenario, calls to window.setMedia()
would bubble up to the browser (so they can keep their UI in sync) and the browser itself would persist the chosen preference for that site.
Let me know what you think in the comments, or hit me up on Twitter.
~
# Spread the word
To help spread the contents of this post, feel free to retweet the announcement tweet:
When implementing Dark Mode and ways to toggle it, developers currently need to duplicate code and roll their own toggle implementation.
— Bram.us (@bramusblog) May 25, 2022
What if, instead, browsers would take care of all that toggle?
🔗 https://t.co/VIazw2a5gB
🏷 #CSS #JavaScript #DarkMode pic.twitter.com/aEKv6mV6Qj
~
🔥 Like what you see? Want to stay in the loop? Here's how:
Some browsers allow for alternative style sheets [1] which would make at least some of your wishes come true and should be a good basis to build upon. There will be duplication of CSS but the way CSS is “written” today I don’t see that as a problem.
[1] https://developer.mozilla.org/en-US/docs/Web/CSS/Alternative_style_sheets
Maybe the auto dark mode origin trial would attempt to solve this.
I have talked about the same here in this article: https://www.amitmerchant.com/auto-dark-mode-origin-trial-of-chrome/#a-game-changer
It’s sad that UI color has regressed more than 30 years.
In the ’90s, you could set up a system-wide color scheme in Windows and Unix GUIs, which all properly-written applications (including browsers) would inherit and use. It was easy for developers to implement, and easy for users to set up a non-inverse (“dark”) color scheme.
Now that everyone has realized that inverse color schemes suck, Microsoft has REMOVED the color-scheme editor from Windows. The vaunted Apple GUI never even supported user-defined color schemes.
Now everyone is running around hard-coding a whopping TWO color schemes, and doing it poorly. People are not getting smarter, folks.
how’s that there’s no reference to the color-scheme meta / property (web.dev/color-scheme)?
it does take a step in the direction of controlling scheme at the page scope.
problem I’ve ran into with it is that it, also, cannot be queried from a css level.
we can query what OS preference user has set, but not what the color-scheme has set