How to override css prefers-color-scheme setting

39,171

Solution 1

I have determined an appropriate solution, it is as follows:

CSS will use variables and themes:

// root/default variables
:root {
    --font-color: #000;
    --link-color:#1C75B9;
    --link-white-color:#fff;
    --bg-color: rgb(243,243,243);
}
//dark theme
[data-theme="dark"] {
    --font-color: #c1bfbd;
    --link-color:#0a86da;
    --link-white-color:#c1bfbd;
    --bg-color: #333;
}

The variables are then called where necessary, for example:

//the redundancy is for backwards compatibility with browsers that do not support CSS variables.
body
{
    color:#000;
    color:var(--font-color);
    background:rgb(243,243,243);
    background:var(--bg-color);
}

JavaScript is used to identify which theme the user has set, or if they have over-ridden their OS theme, as well as to toggle between the two, this is included in the header prior to the output of the html <body>...</body>:

//determines if the user has a set theme
function detectColorScheme(){
    var theme="light";    //default to light

    //local storage is used to override OS theme settings
    if(localStorage.getItem("theme")){
        if(localStorage.getItem("theme") == "dark"){
            var theme = "dark";
        }
    } else if(!window.matchMedia) {
        //matchMedia method not supported
        return false;
    } else if(window.matchMedia("(prefers-color-scheme: dark)").matches) {
        //OS theme setting detected as dark
        var theme = "dark";
    }

    //dark theme preferred, set document with a `data-theme` attribute
    if (theme=="dark") {
         document.documentElement.setAttribute("data-theme", "dark");
    }
}
detectColorScheme();

This javascript is used to toggle between the settings, it does not need to be included in the header of the page, but can be included wherever

//identify the toggle switch HTML element
const toggleSwitch = document.querySelector('#theme-switch input[type="checkbox"]');

//function that changes the theme, and sets a localStorage variable to track the theme between page loads
function switchTheme(e) {
    if (e.target.checked) {
        localStorage.setItem('theme', 'dark');
        document.documentElement.setAttribute('data-theme', 'dark');
        toggleSwitch.checked = true;
    } else {
        localStorage.setItem('theme', 'light');
        document.documentElement.setAttribute('data-theme', 'light');
        toggleSwitch.checked = false;
    }    
}

//listener for changing themes
toggleSwitch.addEventListener('change', switchTheme, false);

//pre-check the dark-theme checkbox if dark-theme is set
if (document.documentElement.getAttribute("data-theme") == "dark"){
    toggleSwitch.checked = true;
}

finally, the HTML checkbox to toggle between themes:

<label id="theme-switch" class="theme-switch" for="checkbox_theme">
    <input type="checkbox" id="checkbox_theme">
</label>

Through the use of CSS variables and JavaScript, we can automatically determine the users theme, apply it, and allow the user to over-ride it as well. [As of the current time of writing this (2019/06/10), only Firefox and Safari support the automatic theme detection]

Solution 2

You can use my custom element <dark-mode-toggle> that initially adheres to the user's prefers-color-scheme setting, but that also allows the user to (permanently or temporarily) override it. The toggle works both with separate CSS files or with classes that are toggled. The README has examples for both approaches.

Solution 3

Not sure, why all answers are so complicated.

Use CSS variables, set a default value, and an opposite value in a media query, as usual. Also set the values in two classes. Implement a toggle that toggles these classes when clicked.

By default, automatic light/dark mode is used based on the system color scheme. Using the toggle switches to manual light/dark mode. It returns to automatic light/dark mode after refreshing the page (or removing the class from the html element).

// toggle to switch classes between .light and .dark
// if no class is present (initial state), then assume current state based on system color scheme
// if system color scheme is not supported, then assume current state is light
function toggleDarkMode() {
  if (document.documentElement.classList.contains("light")) {
    document.documentElement.classList.remove("light")
    document.documentElement.classList.add("dark")
  } else if (document.documentElement.classList.contains("dark")) {
    document.documentElement.classList.remove("dark")
    document.documentElement.classList.add("light")
  } else {
    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
      document.documentElement.classList.add("dark")
    } else {
      document.documentElement.classList.add("light")
    }
  }
}
/* automatic/manual light mode */
:root, :root.light {
  --some-value: black;
  --some-other-value: white;
}

/* automatic dark mode */
/* ❗️ keep the rules in sync with the manual dark mode below! */
@media (prefers-color-scheme: dark) {
  :root {
    --some-value: white;
    --some-other-value: black;
  }
}

/* manual dark mode 
/* ❗️ keep the rules in sync with the automatic dark mode above! */
:root.dark {
  --some-value: white;
  --some-other-value: black;
}

/* use the variables */
body {
  color: var(--some-value);
  background-color: var(--some-other-value);
}
<button onClick="toggleDarkMode()">Toggle</button>
<h1>Hello world!</h1>

Solution 4

Here's an answer that respects the default prefers-color-scheme, and only then lets you toggle via localStorage. This spares the second it takes to figure out the default scheme via JS plus people will use the default scheme even without JS.

I don't like having to declare a default style (I went for Dark) and then re-declaring it as a class called dark-mode but it's unavoidable.

Note this forum seems to block localStorage so you have to try the code somewhere else.

var theme, prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
if (prefersDarkScheme.matches)
    theme = document.body.classList.contains("light-mode") ? "light" : "dark";
else
    theme = document.body.classList.contains("dark-mode") ? "dark" : "light";
localStorage.setItem("theme", theme);

function toggle() {
    var currentTheme = localStorage.getItem("theme");
    if (currentTheme == "dark")
        document.body.classList.toggle("light-mode");
    else if (currentTheme == "light")
        document.body.classList.toggle("dark-mode");
}
.dark-mode {color: white; background-color: black}
.dark-mode a:link {color: DeepSkyBlue}
.light-mode {color: black; background-color: white}
.light-mode a:link {color: green}


@media (prefers-color-scheme: dark) {
    body {color: white; background-color: black}
    a:link {color: DeepSkyBlue}
}
<button onclick="toggle()">Toggle Light/Dark Mode</button>

Solution 5

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.

Share:
39,171
JimmyBanks
Author by

JimmyBanks

Hey

Updated on July 09, 2022

Comments

  • JimmyBanks
    JimmyBanks almost 2 years

    I am implementing a dark mode, as macOS, Windows and iOS have all introduced dark modes.

    There is a native option for Safari, Chrome, and Firefox, using the following CSS media rule:

    @media (prefers-color-scheme: dark) {
    body {
        color:#fff;
        background:#333333
    }
    

    This will automatically identify systems that are set to dark modes, and apply the enclosed CSS rules.

    However; even though users may have their system set to dark mode, it may be the case that they prefer the light or default theme of a specific website. There is also the case of Microsoft Edge users which does not (yet) support @media (prefers-color-scheme. For the best user experience, I want to ensure that these users can toggle between dark and default modes for those cases.

    Is there a method that this can be performed, possibly with HTML 5 or JavaScript? I'd include the code I have tried, but I haven't been able to find any information on implementing this whatsoever!

  • Seth Warburton
    Seth Warburton almost 5 years
    You don't need Javascript, just use a native html element that has 'toggly' behaviour already, a checkbox for example, and then use an adjacent sibling selector, or descendant selector along with the :checked psuedo selector to switch between colour modes.
  • kilian
    kilian almost 4 years
    @SethWarburton This would just work as long as you don't reload the site. If you want to store the users setting, you need JS.
  • DerLauskop
    DerLauskop almost 3 years
    "IE end of life, August 17th 2021 🥳✌️🎉" - what a time to be alive.
  • Yogesh Yadav
    Yogesh Yadav almost 3 years
    Easy to understand. Only thing missing is feature of storing theme in local storage.
  • Gandoe Dyck
    Gandoe Dyck almost 3 years
    Im kind of new to this, i tried copy pasting this code into my wordpress theme but it did nothing, except give me errors: RULE IS EMPTY & EXPECTED A RBRACE. Should i replace '--some-value' with a specific code?
  • Dai
    Dai over 2 years
    @kilian Why not both? Use a script-less checkbox to toggle the theme (so users without JS can use it, while using the change event-listener to save the setting to localStorage - and users without JS can save the setting too if you add a <form> to the checkbox and the server sets a scheme-preference cookie which is used to set the checkbox state in future reloads. JS is more optional than most people think!
  • Julian Knight
    Julian Knight about 2 years
    Excellent answer! But a minor issue. The prefers section of the function has the light and dark settings reversed. Also using ?. doesn't work with all browsers. Better to use if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) - I've done the edit.