How to override css prefers-color-scheme setting

JavascriptHtmlCssMedia Queries

Javascript Problem Overview


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!

Javascript Solutions


Solution 1 - Javascript

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 - Javascript

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 - Javascript

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 - Javascript

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 - Javascript

Took the solution provided by @JimmyBanks and 1) turned the checkbox into a toggling text button, and 2) added automatic theme switching on OS theme change.

CSS is unchanged, with light themes stored in the :root and dark themes stored under [data-theme="dark"]:

:root {
  --color_01: #000;
  --color_02: #fff;
  --color_03: #888;
}

[data-theme="dark"] {
  --color_01: #fff;
  --color_02: #000;
  --color_03: #777;
}

The <head> JS has some edits, including a few omissions and the move of the data-theme statement to the subsequent JS block:

var theme = 'light';
if (localStorage.getItem('theme')) {
  if (localStorage.getItem('theme') === 'dark') {
    theme = 'dark';
  }
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  theme = 'dark';
}

And here is the edit to the second block of JS, plus related HTML. theme_switch toggles the theme, while theme_OS automatically updates the site's theme with changes to the OS theme.

var theme;
function theme_apply() {
  'use strict';
  if (theme === 'light') {
    document.getElementById('theme_readout').innerHTML = 'Dark';
    document.documentElement.setAttribute('data-theme', 'light');
    localStorage.setItem('theme', 'light');
  } else {
    document.getElementById('theme_readout').innerHTML = 'Light';
    document.documentElement.setAttribute('data-theme', 'dark');
    localStorage.setItem('theme', 'dark');
  }
}
theme_apply();
function theme_switch() {
  'use strict';
  if (theme === 'light') {
    theme = 'dark';
  } else {
    theme = 'light';
  }
  theme_apply();
}
var theme_OS = window.matchMedia('(prefers-color-scheme: light)');
theme_OS.addEventListener('change', function (e) {
  'use strict';
  if (e.matches) {
    theme = 'light';
  } else {
    theme = 'dark';
  }
  theme_apply();
});
<a onclick="theme_switch()">Theme: <span id="theme_readout"></span></a>

Please let me know if you have any suggestions for improvement!

Solution 6 - Javascript

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.

Solution 7 - Javascript

TL;DR


index.html
<!DOCTYPE html>
<html>
	<head>
		<meta name="color-scheme" content="light dark">
		<link rel="stylesheet" type="text/css" href="style.css" />
	</head>
	<body>
		<h1>Hello world</h1>
		<button id="toggle">Toggle</button>
		<script type="text/javascript" src="script.js"></script>
	</body>
</html>


style.css
.dark-mode {
	background-color: black;
	color: white;
}

.light-mode {
	background-color: white;
	color: black;
}

@media (prefers-color-scheme: dark) {
    body {
		background-color: black;
		color: white;
    }
}


script.js
/**
 * Adopt:
 * the theme from the system preferences; or
 * the previously stored mode from the `localStorage`
 */
var initialMode = "light";
var prefersColorSchemeDark = window.matchMedia( "(prefers-color-scheme: dark)" );

if ( prefersColorSchemeDark.matches ) {
    initialMode = "dark";
}

if( localStorage.getItem("initialMode") == null ) {
	localStorage.setItem("initialMode", initialMode);
}

if( localStorage.getItem("currentMode") == null ) {
	localStorage.setItem("currentMode", initialMode);
} else {
	let currentMode = localStorage.getItem("currentMode");
    if ( currentMode == "dark" && currentMode != initialMode ) {
        document.body.classList.add("dark-mode");
    } else if ( currentMode == "light" && currentMode != initialMode ) {
        document.body.classList.add("light-mode");
    }
}

/**
 * Process the toggle then store to `localStorage`
 */
document.getElementById('toggle').addEventListener("click", function() {
    var initialMode = localStorage.getItem("initialMode");
    let currentMode = localStorage.getItem("currentMode");

    if ( currentMode == "dark" && currentMode == initialMode ) {
        document.body.classList.add("light-mode");
        localStorage.setItem("currentMode", "light");
    } else if ( currentMode == "light" && currentMode == initialMode ) {
        document.body.classList.add("dark-mode");
        localStorage.setItem("currentMode", "dark");
    } else if ( currentMode != initialMode ) {
        document.body.removeAttribute("class");
        if( currentMode == "dark" ) {
        	localStorage.setItem("currentMode", "light");
        } else {
        	localStorage.setItem("currentMode", "dark");
        }
    }
},
false);

Details

This solution assumes that:

  1. Whatever was set on the system preferences (dark/light mode), that will be acknowledged as the initial mode
  2. From the initial mode, the end-user then can toggle manually either dark mode or light mode
  3. If the system does not have a dark mode feature, the light mode theme will be used
  4. Whatever the theme (dark/light mode) that the end-user manually set previously, that will be the new initial mode on the next page reload/refresh

Solution 8 - Javascript

My Solution (3 options in radio inputs: dark, system, light) adaptation of JimmyBanks and Meanderbilt Solution:

its a bit verbose I guess, but I struggled a bit to wrap my head around it

const themeSwitches = document.querySelectorAll('[data-color-theme-toggle]')

function removeColorThemeLocalStorage() {
  localStorage.removeItem('color-theme')
}

function saveColorTheme(colorTheme) {
  if (colorTheme === 'system') {
    removeColorThemeLocalStorage()
    return
  }
  localStorage.setItem('color-theme', colorTheme)
}

function applyColorTheme() {
  const localStorageColorTheme = localStorage.getItem('color-theme')
  const colorTheme = localStorageColorTheme || null
  if (colorTheme) {
    document.documentElement.setAttribute('data-color-theme', colorTheme)
  }
}

function themeSwitchHandler() {
  themeSwitches.forEach(themeSwitch => {
    const el = themeSwitch
    if (el.value === localStorage.getItem('color-theme')) {
      el.checked = true
    }

    el.addEventListener('change', () => {
      if (el.value !== 'system') {
        saveColorTheme(el.value)
        applyColorTheme(el.value)
      } else {
        removeColorThemeLocalStorage()
        document.documentElement.removeAttribute('data-color-theme')
      }
    })
  })
  applyColorTheme()
}
document.addEventListener('DOMContentLoaded', () => {
  themeSwitchHandler()
  applyColorTheme()
})

html {
  --hue-main: 220;
  --color-text: hsl(var(--hue-main), 10%, 25%);
  --color-text--high-contrast: hsl(var(--hue-main), 10%, 5%);
  --color-link: hsl(var(--hue-main), 40%, 30%);
  --color-background: hsl(var(--hue-main), 51%, 98.5%);
}

@media (prefers-color-scheme: dark) {
  html.no-js {
    --color-text: hsl(var(--hue-main), 5%, 60%);
    --color-text--high-contrast: hsl(var(--hue-main), 10%, 80%);
    --color-link: hsl(var(--hue-main), 60%, 60%);
    --color-background: hsl(var(--hue-main), 10%, 12.5%);
  }
}

[data-color-theme='dark'] {
  --color-text: hsl(var(--hue-main), 5%, 60%);
  --color-text--high-contrast: hsl(var(--hue-main), 10%, 80%);
  --color-link: hsl(var(--hue-main), 60%, 60%);
  --color-background: hsl(var(--hue-main), 10%, 12.5%);
}
	<div class="color-scheme-toggle" role="group" title="select a color scheme">
	<p>saved setting: <span class="theme-readout">...</span></p>
		<input type="radio" name="scheme" id="dark" value="dark" aria-label="dark color scheme"> <label for="dark">dark</label>
		<input type="radio" name="scheme" id="system" value="system" aria-label="system color scheme" checked="system"> <label for="system">system</label>
		<input type="radio" name="scheme" id="light" value="light" aria-label="light color scheme"> <label for="light">light</label>
	</div>

Solution 9 - Javascript

I believe the best way is to natively follow system settings unless user says otherwise.

Create button in your html. And then bind three-position switch to it with js. With saving to browser's LocalStorage.

And, finally, stylize your switch element.

document.addEventListener("DOMContentLoaded", function(event) {
  switchTheme('.theme-switch');
});

function switchTheme(selector) {
  const switches = document.querySelectorAll(selector);
  // let colorTheme = localStorage.getItem('colorTheme') || 'system'; //commented to avoid security issue
  let colorTheme = 'system';

  function changeState() {
    // localStorage.setItem('colorTheme', colorTheme); //commented to avoid security issue
    document.documentElement.setAttribute('data-theme', colorTheme);
  }
  changeState();

  switches.forEach(el => {
    el.addEventListener('click', () => {
      switch (colorTheme) {
        case 'dark':
          colorTheme = 'light';
          break
        case 'light':
          colorTheme = 'system';
          break
        default:
          colorTheme = 'dark';
      }
      changeState();
    });
  });
}

:root:not([data-theme="dark"]) {
  --bg: #fff;
}
@media (prefers-color-scheme: dark) {
   :root:not([data-theme="light"]) {
    --bg: #000;
  }
}
:root[data-theme="dark"] {
  /* yep, you'll need to duplicate styles from above */
  --bg: #000;
}


body {
  background: var(--bg);
}


.theme-switch:after {
  content: ': system';
}
:root[data-theme="dark"] .theme-switch:after {
  content: ': dark';
}
:root[data-theme="light"] .theme-switch:after {
  content: ': light';
}

<button class="theme-switch">Color scheme</button>

Solution 10 - Javascript

I suggest using SCSS. You can make it more simpler.

/* Dark Mode */
@mixin darkMixin {
	body {
		color: #fff; 
		background: #000;
	}
}

@media (prefers-color-scheme: dark) {
	@include darkMixin;
}

.darkMode {
	@include darkMixin;
}

.lightMode {
	body {
		color: #000; 
		background: #fff;
	}
}

And you can toggle/override using JavaScript. (In this example, I used jQuery to make it look easy)

// dark
$('html').removeClass('lightMode').addClass('darkMode')

// light
$('html').removeClass('darkMode').addClass('lightMode')

If you want to detect, this is the code based on JimmyBanks' one.

function isDarkTheme(){
	let theme="light";    //default to light
	if (localStorage.getItem("theme")){
		if (localStorage.getItem("theme") == "dark")
			theme = "dark"
	} else if (!window.matchMedia) {
		return false
	} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
		theme = "dark"
	}
	return theme=='dark'
}

To save the current theme, just use localStorage:

localStorage.setItem("theme", 'light')
or
localStorage.setItem("theme", 'dark')

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJimmyBanksView Question on Stackoverflow
Solution 1 - JavascriptJimmyBanksView Answer on Stackoverflow
Solution 2 - JavascriptDenverCoder9View Answer on Stackoverflow
Solution 3 - JavascriptphilmcoleView Answer on Stackoverflow
Solution 4 - JavascriptLWCView Answer on Stackoverflow
Solution 5 - JavascriptMeanderbiltView Answer on Stackoverflow
Solution 6 - JavascriptamarinediaryView Answer on Stackoverflow
Solution 7 - JavascriptAbel CallejoView Answer on Stackoverflow
Solution 8 - JavascriptscsskidView Answer on Stackoverflow
Solution 9 - JavascriptkeymasterrView Answer on Stackoverflow
Solution 10 - JavascriptExisView Answer on Stackoverflow