Forcing Dark Mode with prefs-color-scheme Media Query

Posted

A few posts ago, I described how to set Dark Mode with the prefs-color-scheme Media Query. The browser will detect the OS Dark Mode setting (set in the Windows Control Panel or macOS System Preferences) and use the appropriate CSS Media Query rule. But what if you want to programmatically set either Dark or Light mode irrespective of the OS setting?

I previously mentioned that I couldn’t figure out a way to do this with JavaScript. Well, no longer! Here’s my solution.

First, note that my style sheet is designed for Light Mode by default, and that I have only one external Style Sheet in my design. For Dark Mode I only have to override a few styles by adding this Media Query as the last CSS rule, e.g.:

@media (prefers-color-scheme: dark) {
 body { background-image:linear-gradient(141deg,#112 0%, #223 20%,#445 100%) !important; color:#aad }
 ...
}

So:

  • By default, the rule automatically fires based on the OS Dark Mode setting.
  • To always set Dark Mode, simply add both prefers-color-scheme: dark and prefers-color-scheme: light. Easy.
  • For the opposite effect, I couldn’t just remove the prefers-color-scheme: dark Media Query - the rule that would always fire, which is certainly not the desired effect! So, I simply rename it instead.
function SetPreferredColorScheme(mode) {
  for (var i = document.styleSheets[0].rules.length-1; i >= 0; i--) {
    rule = document.styleSheets[0].rules[i].media;
    if (rule.mediaText.includes("prefers-color-scheme")) {        
      switch (mode) {
      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;
    }
  }
}

BTW, if you want to get fancy, you could add a listener to detect when the OS-level dark mode setting is changed. Something like this would work (again, since I only a Media Query for dark mode):

window.matchMedia('(prefers-color-scheme: dark)').addListener(
  (media) => { console.debug("Dark Mode = " + media.matches); }
);

Test it here