'Define dark mode for both a class and a media query, without repeat CSS custom properties declarations, and allow users to switch between color modes
I have:
body { background: white; }
To display dark mode, I use .dark class:
.dark body { background: black; }
And to detect if user has their OS set to use dark theme, we have prefers-color-scheme:
@media (prefers-color-scheme: dark) {
body { background: black; }
}
And then we have the idea of DRY (Don’t Repeat Yourself) programming. Can we define dark mode without repeating CSS properties declarations, and in the process, allow users to switch between the color modes via JS?
With the above example, the .dark class and the media query are copies of each other.
What I've done so far
Skipped prefers-color-scheme in CSS and used:
body { background: white; }
.dark body { background: black; }
Then via JS, detect their settings and adjust the
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.getElementsByTagName('html')[0].classList.add('dark');
}
The problem with this approach is it doesn't use prefers-color-scheme in CSS.
While I can add:
@media (prefers-color-scheme: dark) {
body { background: black; }
}
It won't let me toggle the color schemes via JS because I can't cancel prefers-color-scheme: dark for a user who has dark set in their OS preferences.
What is the 2022 way of solving this?
Solution 1:[1]
Below is a simple solution using CSS Variables, prefers-color-scheme media query and radio buttons. Here, the page detects OS dark/light theme and lets the user to change the theme by clicking on radio buttons without JavaScript. You can also trigger a click event on these radio buttons if you really need JavaScript.
/* Defines theme variables */
:root {
--theme-light-color: #222222;
--theme-light-background: #FFFFFF;
--theme-dark-color: #DDDDDD;
--theme-dark-background: #222222;
}
/* Defines body content styles */
body {
margin: 0;
}
body>main {
min-height: 100vh;
color: var(--theme-main-color);
background: var(--theme-main-background);
}
label[for="theme-light"] {
color: var(--theme-light-color);
background: var(--theme-light-background);
cursor: pointer;
}
label[for="theme-dark"] {
color: var(--theme-dark-color);
background: var(--theme-dark-background);
cursor: pointer;
}
/* IF theme = light THEN */
#theme-light:checked~main {
--theme-main-color: var(--theme-light-color);
--theme-main-background: var(--theme-light-background);
}
@media (prefers-color-scheme: light){
:root {
--theme-main-color: var(--theme-light-color);
--theme-main-background: var(--theme-light-background);
}
}
/* IF theme = dark THEN */
#theme-dark:checked~main {
--theme-main-color: var(--theme-dark-color);
--theme-main-background: var(--theme-dark-background);
}
@media (prefers-color-scheme: dark){
:root {
--theme-main-color: var(--theme-dark-color);
--theme-main-background: var(--theme-dark-background);
}
}
<!DOCTYPE html>
<html lang="en">
<body>
<!-- THEME SWITCH -->
<input type="radio" id="theme-light" name="theme" hidden>
<input type="radio" id="theme-dark" name="theme" hidden>
<!-- BODY CONTENT -->
<main>
<label for="theme-light">Light</label>
<label for="theme-dark">Dark</label>
<p>Hello World!</p>
</main>
</body>
</html>
Solution 2:[2]
If you want to do this with ?css?, you can try the following method
:root {
--body-background-color: white;
}
@media (prefers-color-scheme: dark) {
:root {
--body-background-color: black;
}
}
body {
background-color: var(--body-background-color);
}
Solution 3:[3]
You could achieve that by reversing the logic.
body { background: black; }
@media (prefers-color-scheme: light) {
:root:not(.dark) body { background: white; }
}
Solution 4:[4]
You can keep advantage of the CSS media query without actually using it in the CSS itself by using JS.
For example, taking your .dark class from your code, you could use a MediaQueryList object:
const darkMode = window.matchMedia("(prefers-color-scheme: dark)");
And set up a toggle for the class list of the document:
// Initial setting
document.documentElement.classList.toggle("dark", darkMode.matches);
// Listener to changes
darkMode.addListener((event) =>
document.documentElement.classList.toggle("dark", event.matches));
In that way you can focus in your CSS files on just defining the classes under the .dark class and you won't need to define it twice by using the media query. Also, doing it in that way you can setup settings for the user which would override the default OS Preference in case you want (for example saving it in a localStore, etc...)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Baptistou |
| Solution 2 | Reza Mohabbat |
| Solution 3 | Alohci |
| Solution 4 |
