There has been a fresh round of enthusiasm and writing around light mode / dark mode support for the web lately. I think it’s driven partially by the new light-dark()
function in CSS (CSS Color Module Level 5 spec) that makes it easier to declare values that change depending on the mode. Here’s the basic usage:
html {
color-scheme: light dark;
background: light-dark(white, black);
color: light-dark(black, white);
}
Code language: CSS (css)
In real life, you’d probably be using custom properties with your colors. So you’d set up your colors for light mode, then when the special dark mode media query matches, you’d re-declare all those variables, and then you’d use them. Paweł Grzybek has a nice basic explanation. Here’s a comparison.
You used to have to do this:
:root {
color-scheme: light dark;
--dark-color: #292524;
--light-color: #f5f5f4;
--text-color: var(--dark-color);
--bg-color: var(--light-color);
}
@media (prefers-color-scheme: dark) {
:root {
--text-color: var(--light-color);
--bg-color: var(--dark-color);
}
}
body {
color: var(--text-color);
background-color: var(--bg-color);
}
Code language: CSS (css)
And now you can do this:
:root {
color-scheme: light dark;
--light: #292524;
--dark: #f5f5f4;
}
body {
color: light-dark(var(--light), var(--dark));
background-color: light-dark(var(--dark), var(--light));
}
Code language: CSS (css)
Essentially, it prevents you from having to use the @media
query and re-declare variables. I like it. I think it makes code like this more readable and succinct — pending variable naming and usage of course.
Here’s what it’s good for: designing a site that responds to the operating system level setting for light mode / dark mode. And you have to be good with the level of browser support (no Safari just yet, as I write, but it’s in preview). If you are wondering what the fallback is, it’s doing things the old way (see above), and if you’re going to write that you might as well leave it at that.
If you’re going to want to offer more themes than just light and dark, well, you’re out of luck here, you’ll need to implement something else, likely based on changing classes on the <html>
element and updating variables when the class matches.
html.nickleoden-theme {
--bg-color: purple;
--text-color: green;
}
Code language: CSS (css)
I could imagine a more composable toggle method in the future, but this is what we have for now.
When I first saw light-dark()
, I liked the basic idea, but I figured as soon as you offer your own user toggle for mode, you’d be out of luck. After all, you can’t change what the prefers-color-scheme
media query returns, and thus which of the two values light-dark()
will pick. But it turns out you can! The trick lies in that color-scheme
property. You’ll see recommendations that you use color-scheme: light dark;
. If you only set one or the other, you’re forcing light-dark()
to pick the relevant side.
So then your user toggle could force one or the other values on the document if it’s set. (If it’s not set, leave it alone!). Here’s that working:
I like that this is based on a CSS property, as it means that you can use the cascade if you need to, setting the color-scheme
on a per-element basis.
That’s just cool I think.
Anne Sturdivant blogged some recent learnings about all this color theme stuff and I learned a few things. For one, color-scheme
also has normal
which is “no color schemes defined, default to the browser or OS setting”, which I would assume means works the same as light dark
, but in my testing did not switch over to dark when I switched my OS 🤷. Then there is only
as a keyword to, uhm, I guess even more forcibly set the theme? It’s not clear to me, and also not really supported yet anyway.
Annie also clearly points out that when you change the color-theme
away from the default light mode, all sorts of stuff changes. It’s certainly not just the results of light-dark()
. If you set dark
, the UI scrollbars go dark, and all the different form controls go dark. In fact, that might be the number one use-case for color-scheme
really, even if we do have accent-color
now.
There is also this whole idea of System Colors that tends to come up in writing about this, so I’m going to do the same. These are “named” colors, like rebeccapurple
, but they have a fancy baked in ability in that they can change when the color-scheme
changes. So if you want to flip out basically white and black, but not bother with variables and fancy functions and whatnot, you’ve got:
body {
background-color: Canvas;
color: CanvasText;
color-scheme: light dark;
}
Code language: CSS (css)
Note the Canvas
and CanvasText
, those are the System Colors. Allow me to snipe the images of the available colors and what they look like in the different modes from a great article by Mads Stoumann.
Mads made a cool demo of a theme toggler that uses newfangled style queries that is worth checking out. Remember, though, that these client-side-only theme switchers are really just demos. It’s likely on a real-world site, if you’re offering a switcher, you should try to persist that information. A cookie, sessionStorage, localStorage, to a database… something. Then when you do that, there is the risk that the page renders before you can access and set that information, leading to FART, my most successful coining of an acronym ever. It’s a tricky thing, as delaying rendering just for something like this doesn’t feel right.
Another thing percolating in the industry is, as Bramus puts it: What if you had real control over Light Mode / Dark Mode on a per-site basis? I always say that web standards bodies, and even browsers themselves to some degree, are at their best when they see developers toiling and doing the same sort of things, and then introduce better and standardized methods. Properly implementing color themes and the toggling methods with persistence is real work! It doesn’t look like color themes are just a fad, so I think this is a clear opportunity for help.
I think a bit of browser UI would be perfectly welcome:
A perfect implementation would be that the browser itself remembers what choice you’ve made (light, dark, or default to system preference), and that is applied during rendering, avoiding FART. Sounds like it is going to require a new API and then potentially browsers actually using it themselves, which is kinda funny to think about. I normally think about web APIs as being for developers, not browsers.
Thanks for linking to my website Chris 🙏
Sounds like it is going to require a new API and then potentially browsers actually using it themselves,
If browsers were to implement this feature natively, the would not need the Web Preferences API, as they can do it directly.
However, to pass the info to the website, and to allow the website to change the value should they want to, the Web Preferences API is the thing that would allow that.
Also, for my POC extension I rely on the Web Preferences API as that’s currently the thing that allowed me to implement it – there is currently no other way (besides using Chrome DevTools Protocol) to override light/dark on a website.