None of the above suited me. I decided to approach the problem from a different perspective. Year is 2021.
The following offers:
- System preferences respect.
- System preferences overwrite.
- Scrollbars color scheme respect.
- Universal browser support. (IE end of life, August 17th 2021 ✌️)
When you take a look at the MDN Web Docs page for prefers-color-scheme you can read the following:
The prefers-color-scheme CSS media feature is used to detect if the user has requested a light or dark color theme.
[...]
light Indicates that user has notified that they prefer an interface that has a light theme, or has not expressed an active preference.
So for any browsers, by default, the prefers-color-scheme is either set to light or isn't supported.
One of the problem I had with the accepted answer was that the changes were not affecting the scrollbar color. This can be handle using the color-scheme CSS property coupled to the :root pseudo element.
The other problem I had was that, If a user was to change the system settings to light or dark, the website wouldn't be affeted by it and would generate miss-matches between both styles. We can fix that behaviour by coupling window.matchMedia( '(prefers-color-scheme: light)' ) to a onchange event listener.
Here is the final script.
(() => {
var e = document.getElementById("tglScheme");
window.matchMedia("(prefers-color-scheme: dark)").matches
? (document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:dark}</style>'),
document.body.classList.add("dark"),
e && (e.checked = !0),
window.localStorage.getItem("scheme") &&
(document.getElementById("scheme").remove(), document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:light}</style>'), document.body.classList.remove("dark"), e && (e.checked = !1)),
e &&
e.addEventListener("click", () => {
e.checked
? (document.getElementById("scheme").remove(),
document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:dark}</style>'),
document.body.classList.add("dark"),
localStorage.removeItem("scheme"))
: (document.getElementById("scheme").remove(),
document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:light}</style>'),
document.body.classList.remove("dark"),
localStorage.setItem("scheme", 1));
}))
: (document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:light}</style>'),
e && (e.checked = !1),
window.localStorage.getItem("scheme") &&
(document.getElementById("scheme").remove(), document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:dark}</style>'), document.body.classList.add("dark"), e && (e.checked = !0)),
e &&
e.addEventListener("click", () => {
e.checked
? (document.getElementById("scheme").remove(),
document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:dark}</style>'),
document.body.classList.add("dark"),
localStorage.setItem("scheme", 1))
: (document.getElementById("scheme").remove(),
document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:light}</style>'),
document.body.classList.remove("dark"),
localStorage.removeItem("scheme"));
}));
})(),
window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", () => {
location.reload(), localStorage.removeItem("scheme");
});
For the CSS side, we use the default variable custom property values fallback with the dark color in first position. We can define all the necessary dark colors via the :root element.
:root body.dark {
--app-bg-dark: #131313;
--app-tx-dark: #f8f9fa;
}
body{
background-color: var( --app-bg-dark, white );
color: var( --app-tx-dark, black );
}
/* if dark mode isn't set, fall back to light. */
And for the html, a simple checkbox <input id="tglScheme" type="checkbox">.
Finally here is the Codepen https://codepen.io/amarinediary/full/yLgppWW.
⚠️️ Codepen overwrites location.reload() so you won't be abble to test the live update on system change. Don't hesitate to try it on your localhost.