Dark mode with web standards

Lobsters Hottest Tools

Summary

A guide on implementing dark mode on websites using web standards like the color-scheme meta tag and prefers-color-scheme media query, with a JavaScript toggle that respects user preferences and stores them in localStorage.

<p><a href="https://lobste.rs/s/d1hevp/dark_mode_with_web_standards">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 07/05/26, 10:29 AM

# Dark mode with web standards Source: [https://olliewilliams.xyz/blog/dark-mode/](https://olliewilliams.xyz/blog/dark-mode/) Respecting the user’s OS setting is straightforward: use the`prefers\-color\-scheme`media query in CSS\. Arguably, that isn’t enough: users should also be able to customise their choice on a per\-site basis\. A user might want dark mode for the UI of an application, but light mode for reading long\-form text on content\-heavy sites, for example\. We need to: 1. Support the user’s system setting as the default for when the user lands on our website for the first time 2. Allow the user to override their system setting with a toggle in our application\. The color scheme of a web page can be set either via a HTML meta tag in the`<head\>`of the document or via the CSS`color\-scheme`property on the`html`element\. It can take time for CSS to load on slow connections, so using the meta tag is the recommended approach\. When a user lands on your website for the first time, respect their system preference by setting`<meta name="color\-scheme" content="light dark"\>`\. To override the OS setting via a control in your web app or site, use JavaScript to update the`content`attribute value to`light`to force light mode,`dark`to force dark mode, or`light dark`to revert back to the OS setting\. ``` const metaTag = document.querySelector('[name="color-scheme"]'); const savedScheme = localStorage.getItem("colorScheme"); if (savedScheme) {metaTag.setAttribute('content', savedScheme);} btnlight.addEventListener('click', function() { metaTag.setAttribute('content', 'light'); localStorage.setItem("colorScheme", "light"); }); btndark.addEventListener('click', function() { metaTag.setAttribute('content', 'dark'); localStorage.setItem("colorScheme", "dark"); }); btnsystem.addEventListener('click', function() { metaTag.setAttribute('content', 'light dark'); localStorage.removeItem("colorScheme"); }); ``` #### What does the`color\-scheme`affect? - Colors, gradients, or images set via the`light\-dark\(\)`CSS function - System colors like`Canvas`and`CanvasText` - Scrollbar colors - The default colors of HTML elements like buttons - iframes styles \(so long as the iframe document has opted in via the meta tag\) - SVG’s that make use of`light\-dark\(\)`or`prefers\-color\-scheme` #### What doesn’t`color\-scheme`affect? There’s an unfortunate disconnect between`color\-scheme`and the`prefers\-color\-scheme`media query\.`prefers\-color\-scheme`reflects the OS settings — regardless of the`color\-scheme`value\. If you’re providing an in\-page toggle that implements dark mode, you can’t adopt the`prefers\-color\-scheme`media query\. The following code is not be impacted by`color\-scheme`: ``` <picture> <source srcset="logo-dark.png" media="(prefers-color-scheme: dark)" /> <img src="logo-light.png" alt="Product logo" /> </picture> ``` Other than making use of a`background\-image`, there’s sadly not an equivalent approach to the`<picture\>`element that references the`color\-scheme`\. There are two exceptions where`color\-scheme`will affect the`prefers\-color\-scheme`media query: - iframes - SVG Below are two iframes that show the same document\. The document inside the iframe is making use of the`prefers\-color\-scheme`media query\. ``` <iframe style="color-scheme: light;" src="/example.html"></iframe> <iframe style="color-scheme: dark;" src="/example.html"></iframe> ``` As you can see in the above example, the`@media \(prefers\-color\-scheme: dark\)`styles are being applied when`color\-scheme: dark`is used in the parent document\. The same principle applies to SVG\. Here’s the content of an`\.svg`file: ``` <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"> <style> circle { fill: rgb(40,40,40); } @media (prefers-color-scheme: dark) { circle { fill: rgb(216, 216, 216); } } </style> <circle cx="50" cy="50" r="50" /> </svg> ``` ``` <img style="color-scheme: light;" src="/circle.svg" alt=""> <img style="color-scheme: dark;" src="/circle.svg" alt=""> ``` ![](https://olliewilliams.xyz/circle-icon.svg)![](https://olliewilliams.xyz/circle-icon.svg) The CSS spec has recently been[updated](https://github.com/w3c/csswg-drafts/pull/13857#:~:text=This%20PR%20applies%20the%20resolution%20from%20the%20recent%20F2F%20in%20#13377%2C%20making%20(prefers%2Dcolor%2Dscheme)%20reflect%20the%20effects%20of%20%3Cmeta%20name=color%2Dscheme%3E)so that the color\-scheme of the document does effect the media query in all contexts, but no browser has implemented this\. #### Some Safari caveats - Support for the`prefers\-color\-scheme`media query within SVG was added in[Safari 27](https://developer.apple.com/documentation/safari-release-notes/safari-27-release-notes?changes=la,la#:~:text=175598175)-,Fixed%20an%20issue%20where,the%20system%20color%20appearance,-.%20(176413340), but`color\-scheme`does not affect the media query \([see bug report](https://bugs.webkit.org/show_bug.cgi?id=316640)\) - Support for the`prefers\-color\-scheme`media query within iframes was added in[Safari 27](https://developer.apple.com/documentation/safari-release-notes/safari-27-release-notes?changes=la,la#:~:text=140674753)-,Fixed%20an%20issue%20where,was%20set%20to%20dark,-.%20(142072593)and the`color\-scheme`of the parent document does override it, as it should 🎉\. However, other bugs remain \([see bug report](https://bugs.webkit.org/show_bug.cgi?id=316680)\) ## Using`light\-dark\(\)`with images and gradients The`light\-dark\(\)`function was originally limited to colors\. It can now be used for gradients and images \(as of Chrome/Edge version 150, Firefox version 150, and Safari Technology Preview\)\. ``` .bg-gradient { background-image: light-dark(linear-gradient(15deg, #b9b6ff, #308dc6), linear-gradient(15deg, #6b7495, #001339)); } ``` ``` <div class="bg-gradient" style="color-scheme: light;"></div> <div class="bg-gradient" style="color-scheme: dark;"></div> ``` It’s also possible to switch between a single solid color and a gradient, depending on the`color\-scheme`\. ``` .bg-grad-solid { background-image: light-dark(linear-gradient(15deg, #b9b6ff, #308dc6), image(#001339)); } ``` ### Images ``` .bg { background-image: light-dark(url(/lightmode.avif), url(/darkmode.avif)); } ``` ## Changing more than colors, images and gradients By and large, the thing you need to change between modes is color, but there are exceptions\. A`box\-shadow`might not be visible on a dark background in dark mode, so you may decide to apply a border instead, for example\. Implementing that is currently rather challenging\. The CSS standards body is planning to add a way to[detect the current color\-scheme](https://github.com/w3c/csswg-drafts/issues/10577#issuecomment-3329616811)using either a CSS`if\(\)`statement or a style query, but no browser has implemented this feature\. It’s possible to hack together an alternative\. Below I’ll look at two approaches\. Define a CSS variable to be true when the page is using dark mode: ``` html { --dark: false; } html:has([content="light dark"]) { @media (prefers-color-scheme: dark) { --dark: true; } } html:has([content="dark"]) { --dark: true; } ``` All browsers now support style queries, which allow you to apply styles depending on the value of a custom property: ``` @container style(--dark: false) { .card { box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.2); } } @container style(--dark: true) { .card { border: solid 1px rgb(94, 94, 94); } } ``` A better approach that also uses style queries would be: ``` @property --usedScheme { syntax: "<color>"; inherits: true; initial-value: transparent; } body { --usedScheme: light-dark(white, black); } @container style(--usedScheme: white) { .card { box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.2); } } @container style(--usedScheme: black) { .card { border: solid 1px rgb(94, 94, 94); } } ``` ## A \(possible\) future: overriding`prefers\-color\-scheme`via JS We might get a way to override the`prefers\-color\-scheme`media query with JavaScript in the future\. There’s a[spec](https://drafts.csswg.org/mediaqueries-5/#script-control-user-prefs), an[MDN entry](https://developer.mozilla.org/en-US/docs/Web/API/User_Preferences_API)and a prototype in Chrome Canary but the Safari team are[opposed to the idea](https://github.com/WebKit/standards-positions/issues/252)\.

Similar Articles

Six Levels of Dark Mode (2024)

Hacker News Top

An article exploring six levels of dark mode implementation in web design, ranging from barebone meta tag approaches to advanced color scheme switching techniques using CSS.

Algorithmic Theming Engines

Hacker News Top

The article introduces the new CSS `contrast-color()` function, which allows developers to automatically choose black or white text for accessible contrast against any background color, addressing the persistent issue of low contrast on the web without relying on JavaScript.

readable.css

Lobsters Hottest

readable.css is a CSS framework that provides a sensible and beautiful base default for websites, with features like light/dark mode, responsive design, and vertical rhythm, emphasizing consistency and semantic HTML.