Earlier this year, I rolled out . My implementation was pretty straightforward. When the user clicks on the “dark mode” toggle in the site’s header, that triggers some JS which adds a darkMode class to the element, which then applies a set of “dark” styles across the entire design.
Using this approach, your CSS might look something like this, with the “light” styles specified normally and the “dark” styles nested under a selector:
body {
background-color: #fff;
color: #000;
}
html.darkMode {
body {
background-color: #000;
color: #fff;
}
}
There’s nothing inherently wrong with this approach. It obviously works and it’s nice to have all of your “dark” styles in one place. But when extrapolated across an entire website, this approach inevitably results in a lot of duplicate CSS. For every selector that has a “light” style associated with it, you need to create a “dark” version of it, which feels gross and inefficient. (After all, one of the cardinal rules of programming is .)
I didn’t know any other way of achieving a dark mode, though. Until I discovered , that is. Essentially, the light-dark function lets you specify two color values — one “light” and one “dark” — and it then automatically applies the appropriate value based on the currently selected color scheme. Using this function, we can update our CSS thusly:
:root {
color-scheme: light dark;
}
body {
background-color: light-dark(#fff, #000);
color: light-dark(#000, #fff);
}
Now, the element’s colors will be automatically set based on the user’s system preferences. In “light” mode, the site will have a white background with black text, and in “dark” mode, it’ll have a black background with white text.
With Opus, however, the color scheme is set by the aforementioned “dark mode” toggle. Which meant that I had to make an additional tweak to the CSS:
:root {
color-scheme: light dark;
}
html {
color-scheme: light;
&.darkMode {
color-scheme: dark;
}
}
body {
background-color: light-dark(#fff, #000);
color: light-dark(#000, #fff);
}
By default, the color scheme is set to light. But when the darkMode class is added to the element via JS, the color scheme is set to dark, with the light-dark function still applying the proper color values. This means no more “dark” versions of CSS selectors and the resulting duplicate code.
Even better, the light-dark function also accepts CSS variables and other CSS functions like color-mix in addition to normal CSS color values, which makes it truly flexible.
body {
background-color: light-dark(var(--white), var(--black));
color: light-dark(var(--black), var(--white));
}
body {
background-color: light-dark(var(--gray), color-mix(in srgb, var(--black) 10%, var(--gray)));
color: light-dark(color-mix(in srgb, var(--black) 10%, var(--gray)), var(--gray));
}
That said, the light-dark function does have some downsides, as noted on . Specifically, you’re only limited to two color schemes, as per the function’s name. If you want to implement a third scheme (e.g., a “high contrast” scheme), then the light-dark function won’t help you there. Not in its current form, anyway, though the W3C is currently considering ways to extend light-dark to handle more than just two color schemes, as well as methods for specifying non-color-related properties.
That said, light-dark is still awfully handy even in its current “limited” form. It does a lot of heavy-lifting for you while allowing you to write cleaner, more efficient CSS. In other words, it’s definitely something you should consider using the next time you need to implement a dark mode for a website.