I tried implementing this, and achieved some success, but I'm rolling the feature back. I want to preserve some notes from what I've done though in case this is useful in the future.
So, the browser-native way of implementing light and dark mode is usually done by having a normal stylesheet for 'light' mode, and then creating a media query like so
The browser knows to apply this style based on the browser/OS settings. So far so good.
Introducing a feature to let users toggle light and dark, within a site, is very hard though. Here's a StackOverflow thread with answers. The main thing to notice is that most of the answers don't actually work with @media. They expect you to create top level .light and .dark classes, and the Javascript just switches between those.
With enough clues there and in blog posts I was able to cobble together this JS which does allow switching the theme and still let you use the @media queries. Toggling a theme stores the selected theme in local storage and uses that the next time the page loads.
// https://stackoverflow.com/questions/56300132/how-to-override-css-prefers-color-scheme-setting
// Return the system level color scheme, but if something's in local storage, return that
// Unless the system scheme matches the the stored scheme, in which case... remove from local storage
function getPreferredColorScheme(){
let systemScheme = 'light';
if(window.matchMedia('(prefers-color-scheme: dark)').matches){
systemScheme = 'dark';
}
let chosenScheme = systemScheme;
if(localStorage.getItem("scheme")){
chosenScheme = localStorage.getItem("scheme");
}
if(systemScheme === chosenScheme){
localStorage.removeItem("scheme");
}
return chosenScheme;
}
// Write chosen color scheme to local storage
// Unless the system scheme matches the the stored scheme, in which case... remove from local storage
function savePreferredColorScheme(scheme){
let systemScheme = 'light';
if(window.matchMedia('(prefers-color-scheme: dark)').matches){
systemScheme = 'dark';
}
if(systemScheme === scheme){
localStorage.removeItem("scheme");
}
else {
localStorage.setItem("scheme", scheme);
}
}
// Get the current scheme, and apply the opposite
function toggleColorScheme(){
let newScheme = "light";
let scheme = getPreferredColorScheme();
if (scheme === "light"){
newScheme = "dark";
}
applyPreferredColorScheme(newScheme);
savePreferredColorScheme(newScheme);
}
// Apply the chosen color scheme by traversing stylesheet rules, and applying a medium.
function applyPreferredColorScheme(scheme) {
for (var i = 0; i <= document.styleSheets[0].rules.length-1; i++) {
rule = document.styleSheets[0].rules[i].media;
if (rule && rule.mediaText.includes("prefers-color-scheme")) {
switch (scheme) {
case "light":
rule.appendMedium("original-prefers-color-scheme");
if (rule.mediaText.includes("light")) rule.deleteMedium("(prefers-color-scheme: light)");
if (rule.mediaText.includes("dark")) rule.deleteMedium("(prefers-color-scheme: dark)");
break;
case "dark":
rule.appendMedium("(prefers-color-scheme: light)");
rule.appendMedium("(prefers-color-scheme: dark)");
if (rule.mediaText.includes("original")) rule.deleteMedium("original-prefers-color-scheme");
break;
default:
rule.appendMedium("(prefers-color-scheme: dark)");
if (rule.mediaText.includes("light")) rule.deleteMedium("(prefers-color-scheme: light)");
if (rule.mediaText.includes("original")) rule.deleteMedium("original-prefers-color-scheme");
break;
}
}
}
// Change the toggle button to be the opposite of the current scheme
if(scheme === "dark"){
document.getElementById("icon-sun").style.display='inline';
document.getElementById("icon-moon").style.display='none';
}
else {
document.getElementById("icon-moon").style.display='inline';
document.getElementById("icon-sun").style.display='none';
}
}
applyPreferredColorScheme(getPreferredColorScheme());
The JS works but I don't actually understand it, it's using mediaText feature, for which I cannot find any good documentation. It seems to have been hastily implemented by browsers.
And although this works, it's still plagued by another issue, the 'white/black flash' - for example if a user has selected a dark theme, and it's in local storage, there's a brief flash of white background before the JS eventually kicks in and applies the CSS across all the rules.
There is a hacky fix for it but it's one hack too far. It's basically a huge amount of effort, questionable code, and hacky practices, for one small feature. I'd like to stick to CSS best practices because CSS is also hard.
Hopefully in the future this situation improves and there's a more direct, simple way of letting JS set the media for the site, or maybe browsers introduce that feature. In the meantime there's also this extension.
I tried implementing this, and achieved some success, but I'm rolling the feature back. I want to preserve some notes from what I've done though in case this is useful in the future.
So, the browser-native way of implementing light and dark mode is usually done by having a normal stylesheet for 'light' mode, and then creating a media query like so
The browser knows to apply this style based on the browser/OS settings. So far so good.
Introducing a feature to let users toggle light and dark, within a site, is very hard though. Here's a StackOverflow thread with answers. The main thing to notice is that most of the answers don't actually work with
@media
. They expect you to create top level.light
and.dark
classes, and the Javascript just switches between those.With enough clues there and in blog posts I was able to cobble together this JS which does allow switching the theme and still let you use the
@media
queries. Toggling a theme stores the selected theme in local storage and uses that the next time the page loads.On the page I put some SVG icons like so:
Then this Javascript:
The JS works but I don't actually understand it, it's using mediaText feature, for which I cannot find any good documentation. It seems to have been hastily implemented by browsers.
And although this works, it's still plagued by another issue, the 'white/black flash' - for example if a user has selected a dark theme, and it's in local storage, there's a brief flash of white background before the JS eventually kicks in and applies the CSS across all the rules.
There is a hacky fix for it but it's one hack too far. It's basically a huge amount of effort, questionable code, and hacky practices, for one small feature. I'd like to stick to CSS best practices because CSS is also hard.
Hopefully in the future this situation improves and there's a more direct, simple way of letting JS set the media for the site, or maybe browsers introduce that feature. In the meantime there's also this extension.