mendhak / eleventy-satisfactory

Content-first eleventy blog template with basic layout and various features
https://code.mendhak.com/eleventy-satisfactory/
MIT License
20 stars 1 forks source link

The trouble with toggling light/dark mode #2

Closed mendhak closed 1 year ago

mendhak commented 1 year ago

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

@media (prefers-color-scheme: dark) {
:root {
  --bg: #333333;
  --accent: #fedb8b;
}
}

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:

    <a href="javascript:toggleColorScheme();">
      <svg id="icon-sun" viewBox="0 0 36 36"><g fill="#FFAC33"><path d="M16 2s0-2 2-2 2 2 2 2v2s0 2-2 2-2-2-2-2V2zm18 14s2 0 2 2-2 2-2 2h-2s-2 0-2-2 2-2 2-2h2zM4 16s2 0 2 2-2 2-2 2H2s-2 0-2-2 2-2 2-2h2zm5.121-8.707s1.414 1.414 0 2.828-2.828 0-2.828 0L4.878 8.708s-1.414-1.414 0-2.829c1.415-1.414 2.829 0 2.829 0l1.414 1.414zm21 21s1.414 1.414 0 2.828-2.828 0-2.828 0l-1.414-1.414s-1.414-1.414 0-2.828 2.828 0 2.828 0l1.414 1.414zm-.413-18.172s-1.414 1.414-2.828 0 0-2.828 0-2.828l1.414-1.414s1.414-1.414 2.828 0 0 2.828 0 2.828l-1.414 1.414zm-21 21s-1.414 1.414-2.828 0 0-2.828 0-2.828l1.414-1.414s1.414-1.414 2.828 0 0 2.828 0 2.828l-1.414 1.414zM16 32s0-2 2-2 2 2 2 2v2s0 2-2 2-2-2-2-2v-2z"/><circle cx="18" cy="18" r="10"/></g><title>Click for light theme</title></svg>
      <svg id="icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M10.895 7.574c0 7.55 5.179 13.67 11.567 13.67 1.588 0 3.101-.38 4.479-1.063-1.695 4.46-5.996 7.636-11.051 7.636-6.533 0-11.83-5.297-11.83-11.83 0-4.82 2.888-8.959 7.023-10.803a16.16 16.16 0 0 0-.188 2.39z"/><title>Click for dark theme</title></svg>
    </a>

Then this Javascript:

// 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.

mendhak commented 1 year ago

https://code.mendhak.com/css-dark-mode-toggle-sucks/