Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The trouble with toggling light/dark mode #2

Closed
mendhak opened this issue Jan 15, 2023 · 1 comment
Closed

The trouble with toggling light/dark mode #2

mendhak opened this issue Jan 15, 2023 · 1 comment

Comments

@mendhak
Copy link
Owner

mendhak commented Jan 15, 2023

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
Copy link
Owner Author

mendhak commented May 10, 2023

@mendhak mendhak closed this as completed May 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant