Load the menuLoad the menu
Logic and
Language


Copyright   James R Meyer    2012 - 2025 https://jamesrmeyer.com

The Gold Standard Dark Mode Switch for web-sites

  Completely new version 03 Feb 2025

 

Wouldn’t it be great if you could add a Dark/Light switch to your website without having to use hacks like additional ids or classes that you have to incorporate into your CSS code? The general consensus has been that this is impossible - but that is simply incorrect. This page explains how you can add a Dark/Light switch which uses only JavaScript and requires no modification whatsoever of your CSS code. Note that many people have simply created their own Dark mode which overrides the browser/system setting and gives the user no option to revert to the browser/system theme, which is regrettable.

 

The previous method that I outlined on this site required, like all the other methods, modifications to the CSS code, requiring you to remember what those modifications are every time you change or add to your CSS code, so I no longer recommend that method. The previous method was setup so that when JavaScript was disabled, the page would respect the system setting - that is, if the system was set to dark and JavaScript was disabled, the page would display in Dark mode, and if the system was set to light and JavaScript was disabled, the page would display in Light mode. While this worked as intended, it entailed having to write a duplicate CSS declaration inside a media query for every CSS Dark/Light change, and this turned out to be a bit of a hassle every time one wanted to change the CSS or add new CSS, making it prone to errors in such duplication. You can see the previous method at How to setup Dark mode - Previous Version.

 

Having used my previous method for a while, I became dissatisfied with it, since though it worked perfectly, every time I had to change some CSS or add some new CSS, I had to remember what modifications I had to do to the CSS when doing so. I thought that there had to be some sort of trick that would enable a Dark/Light switch without any hack on the CSS code. If you could trick the browser into thinking that the user has set his browser to be in Dark mode for that website. But there doesn’t seem to be any way to do this, so I had to think of an alternative “trick” - the alternative is the method described on this page.

 

With this trick, we can simply write the CSS as if we weren’t adding a Dark/Light switch all - we simply write the CSS with a media query section that modifies various class properties if the system preference is set to Dark mode.

 

For example:, the code below will show the text of the class someClassName as blue if the system preference is set to Light, and will show as red if the system preference is set to Dark:

.someClassName {color : blue}
@media (prefers-color-scheme: dark) {
	.someClassName {color : red}
}

The trick is to use JavaScript to locate this media query rule, and we use JavaScript to change the value of the media query from prefers-color-scheme: dark to prefers-color-scheme: light, so that the browser now sees:

.someClassName {color : blue}
@media (prefers-color-scheme: light) {
	.someClassName {color : red}
}

So if the system preference was set to Light mode and the user clicks the switch to Dark, this tricks the browser into applying the rules within the media query. Similarly, if the system preference is set to Dark mode, but the user wants to override the system mode to give Light mode, we similarly change the media query from prefers-color-scheme: dark to prefers-color-scheme: light.

 

The advantages of this method are:

  1. It is very easy to set up, just incorporate the JavaScript code provided here - the hard work behind the scenes has been done for you..
  2. It requires no modifications of the CSS code, with no additional ids or classes.
  3. You can have as many prefers-color-scheme: dark and prefers-color-scheme: light sections as you want in your CSS files and in <style> tags in your web-page document. You can even include both prefers-color-scheme: dark and prefers-color-scheme: light sections in your code, though if possible it would be better to avoid doing this, as it could make the CSS harder to maintain (you can see an example on my demo page).  Note that the code cannot access the prefers-color-scheme rules in CSS files from external domains due to Cross-Origin Resource Sharing issues unless you add additional code, see for example, https://stackoverflow.com/questions/71327187/cannot-access-cssrules-for-stylesheet-cors.
  4. It defaults to the system preference if JavaScript is disabled or non functional for any reason.
  5. Any future changes to the CSS are very simple to perform, since you simply write the CSS code as if you were not using JavaScript at all - once you have this system setup, you don’t have to remember anything about it when you make any changes to your CSS.
  6. It respects all possible visitor preferences.

The disadvantage is that it will soon be superseded by newer CSS which allows for a superior Dark/Light switch without using any JavaScript at all, the caveats against it is that over 10% of users are using browsers that do not support the newer CSS, and there is no inbuilt safe fall-back for older browsers. But within two years or less, it would seem that this new CSS method will rule the roost. For further information, see CSS color-scheme-dependent colors with light-dark().

 

The basics of Dark and Light modes

This section explains the basic functioning of Dark and Light modes (if you are already au fait with the subject you may want to skip this section). The first thing to note is that there are two possible Dark modes available to the user:

  1. where the user has selected the overall browser/system to be in Dark mode, and
  2. where the user has selected a Dark or Light mode for a particular website by clicking a button on the page

Note that the browser’s Dark mode can be triggered by the operating system’s Dark mode, or, depending on the browser, it may be set independently of the overall system. In the following, the browser/system mode may be referred to simply as the “system” (since we are trying to respect the user’s intentions, note that the user may override their operating system setting in their browser, but this will be seen by the web-page as the “system”).

 

If a web-page has a CSS media query section defined by, for example:

@media (prefers-color-scheme: dark) {
	/* Code for Dark mode */
	.someClassName {color : red}
}

then if the user has set the system mode as “dark”, then the page will display according to the code within this @media section.

 

You don’t need to know in detail how the JavaScript works, you can simply add the JavaScript head code inside the top of the HTML head of each of your web-pages - this head code should come before anything that loads a resource such as CSS files, in order to prevent page flickering on page load. You also need to add a link to the JavaScript file, to prevent it blocking page loading, either put it at the bottom of your HTML code, or add it inside the head with the attribute "defer".

 

As for the details of writing CSS code for Dark mode, many people have suggested using CSS variables to implement a Dark mode. I haven’t used them myself, principally because it would involve a lot of work to change the setup I already had. But if you are only using a small total color set, or are starting a new site from scratch, variables are probably a good option.

 

In any case, with this method you can choose to use CSS variables or not, or a combination, and both methods are demonstrated here. If you are using CSS variables, you can declare the default value as scoped within :root, for example:

:root {
	--primary-color#14172b;
	--secondary-color#f6f4ec;
}

and you set the variable properties for Dark mode within the “@media (prefers‑color‑scheme: dark)” section, for example:

@media (prefers-color-scheme: dark) {
	:root {
	--primary-color#f6f4ec;
	--secondary-color#14172b;
	}
}

 

Note that the above assumes that you are writing your code where the default mode for the web-page is Light mode. If you want the default mode to be Dark mode, then you simply put any property changes for Light mode within a “@media (prefers‑color‑scheme: light)” section.

 

Details of the Code

The essential code required to provide the functionality of the method is quite small and is described below. You can see a demo of the essential files at Basic Dark mode Demo page.

 

You can download copies of the basic demo files here:

 

And there are also demo files that include a message explaining how to revert to the system Dark/Light mode, you can download copies of these demo files here:

 

The JavaScript Code In the Header

If a user chooses Dark mode for one page of your website, the expectation is that they will also want Dark mode for the other pages on your site, so the JavaScript is setup to store the user preference in the browser’s Local Storage. Now, if we wait until the page is loaded before any JavaScript code executes, if the page was loaded in Light mode, but the user wants Dark mode, then the page will load in Light mode and then flicker from Light to Dark as it implements the change. This flicker will be worse on slow connections.

 

To minimize this problem, a small script is required in the head. This reduces the flicker effect by inverting all colors (except for any images) until the document is loaded, giving a temporary approximate Dark or Light effect as appropriate until the page is fully loaded, when the defined Dark/Light mode will be applied. There is no need to worry about a hit on performance as the amount of code required for this is very small.

 

The code in the head is:

document.documentElement.className = "hasJS";
let Gtheme = "system";
let GoverridesDL = false;
switch ((Gtheme = (Gtheme = localStorage.getItem("LStheme")) || "system")) {
	case 'system':
		break;
	case 'light':
		if (window.matchMedia('(prefers-color-scheme: dark)').matches){
			let style = document.createElement('style');
			style.type = 'text/css';
			style.innerHTML = '.invertJS{filter: invert(1);} .invertJS img{filter: invert(1) !important}';
			document.getElementsByTagName('head')[0].appendChild(style);
			document.getElementsByTagName('html')[0].classList.add('invertJS');
			GoverridesDL = !GoverridesDL;
		}
		break;
	case 'dark':
		if (window.matchMedia('(prefers-color-scheme: light)').matches){
			let style = document.createElement('style');
			style.type = 'text/css';
			style.innerHTML = '.invertJS{filter: invert(1);} .invertJS img{filter: invert(1) !important}';
			document.getElementsByTagName('head')[0].appendChild(style);
			document.getElementsByTagName('html')[0].classList.add('invertJS');
			GoverridesDL = !GoverridesDL;
		}
}

and the minified version is:

document.documentElement.className="hasJS";let Gtheme="system",GoverridesDL=!1;switch(Gtheme=(Gtheme=localStorage.getItem("LStheme"))||"system"){case"system":break;case"light":if(window.matchMedia("(prefers-color-scheme: dark)").matches){let e=document.createElement("style");e.type="text/css",e.innerHTML=".invertJS{filter: invert(1);} .invertJS img{filter: invert(1) !important}",document.getElementsByTagName("head")[0].appendChild(e),document.getElementsByTagName("html")[0].classList.add("invertJS"),GoverridesDL=!GoverridesDL}break;case"dark":if(window.matchMedia("(prefers-color-scheme: light)").matches){let e=document.createElement("style");e.type="text/css",e.innerHTML=".invertJS{filter: invert(1);} .invertJS img{filter: invert(1) !important}",document.getElementsByTagName("head")[0].appendChild(e),document.getElementsByTagName("html")[0].classList.add("invertJS"),GoverridesDL=!GoverridesDL}}

 

The Main JavaScript Code

In the following the JavaScript for implementing the initial setup is described. This file can be put at the end of the HTML code, or in the head with the defer option. Once the document is loaded we have the function that does the initial setup:

let GdarkSW; // Global variable to refer to the light/dark switch element
let GdarkLightRules = []; // Global array variable to hold all instances of prefers-color-scheme rules in the CSS

document.addEventListener('DOMContentLoaded', function () {

	// find all dark light media queries in the CSS
	findDarkLightRules();
	// collect all dark/light media rules
	GdarkLightRules = Array.from(findDarkLightRules());
	
	// Remove temporary filter on html tag if it exists, and swap rules if necessary to prevent page flicker on slow load
	document.getElementsByTagName('html')[0].classList.remove('hasJS','invertJS');
	if (GoverridesDL) {swapDarkLight();}

		// Event listeners
	try {
		// add an event listener to the toggle check-box which will detect user click
		// so that if the check-box is clicked, the code will be run to switch the theme
		GdarkSW = document.getElementById('Theme-switcher');		
		GdarkSW.addEventListener('click', function (e) { switchTheme(); }, false );

		// Add listeners to detect changes in system dark/light mode settings, and if so calls the SystemThemeChanged function
		window.matchMedia('(prefers-color-scheme: dark)').addEventListener("change", e => e.matches && SystemThemeChanged());
		window.matchMedia('(prefers-color-scheme: light)').addEventListener("change", e => e.matches && SystemThemeChanged() );

		// We have a button to reset the mode to the system setting rather than user override
		// This adds a listener to the button to detect a click and call the function resetTheme
		document.getElementById('ResetTheme').addEventListener('click', function () { resetTheme(); }, false );

	} catch (e) {
			// console.log(e + ' line: ' + e.lineNumber + ' File: ' + e.fileName);
	}

	// Set the initial state of the check-box
	setCheckBox();

	// Set the initial state of the reset button
	resetThemeButton();
	
});

 

Next we have a function “setCheckBox” that sets the check-box to be correctly checked or unchecked according to whether the display is in dark or Light mode, and a function “resetThemeButton” that sets the Reset Button to enabled or disabled as appropriate.

 

If you want the style of your toggle switch to change according to whether the page display is dark or light, then the “setCheckBox” function is required. On the other hand, if your toggle switch is to appear the same in both cases, then this function is not required. Note that in most cases, you will not want the check-box to be seen, and this is achieved by using CSS position: absolute and positioning it outside of the visible view-port (or setting the opacity: zero and pointer‑events: none).

function setCheckBox(){
	try {
		console.log ('Gtheme :' + Gtheme);
		console.log ('GoverridesDL :' + GoverridesDL);
		// only cases where we are with dark display are if in system mode and prefers dark or when the user theme is dark
		// for these cases we set the check-box to unchecked, otherwise checked
		GdarkSW.checked = true;
		if (Gtheme === 'dark'){ GdarkSW.checked = false; }
		// if we get an error in retrieving matchMedia, defaults to system light mode
		if(Gtheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches){
		 GdarkSW.checked = false;
		 
		}
	} catch (e) {
			// console.log(e + ' line: ' + e.lineNumber + ' File: ' + e.fileName);
	}
}

// Function to setup the reset button
function resetThemeButton() {
	if (Gtheme === 'system') {
		document.getElementById('ResetTheme').classList.add('grayed');
		document.getElementById('ResetTheme').classList.add('disabled');
	}
	else{
		document.getElementById('ResetTheme').classList.remove('grayed');
		document.getElementById('ResetTheme').classList.remove('disabled');
	}
}

 

Next we have a function “resetTheme” that resets the mode to the system default and sets the check box and Reset Button accordingly:

function resetTheme() {
	try {
		// if current system pref is light and current Gtheme is dark, need to swap the prefs
		if ((window.matchMedia('(prefers-color-scheme: light)').matches && Gtheme === 'dark') ||
		(window.matchMedia('(prefers-color-scheme: dark)').matches && Gtheme === 'light')){
			swapDarkLight();
		}
		// if current system pref is dark and current Gtheme is light, need to swap the prefs
		// if (window.matchMedia('(prefers-color-scheme: dark)').matches && Gtheme === 'light') {
			// swapDarkLight();
		// }
	} catch (e) {
			// console.log(e + ' line: ' + e.lineNumber + ' File: ' + e.fileName);
	}
	
	// Now set Gtheme
	Gtheme = 'system';
	
	// reset the reset button
	resetThemeButton();
	
	// need to make check-box switch display correctly
	setCheckBox();
	
	localStorage.removeItem('LStheme');	
}	

 

And we have a function “SystemThemeChanged” which is triggered if the system light/Dark mode is changed by the user - if the page is currently in system mode then the check-box may need to be set correctly. We also need to force a page refresh, otherwise the change in mode will not be displayed.

// Function for if there is a system change
function SystemThemeChanged() {
	switch(Gtheme) {
		case 'system': // need to set check-box if page is in system mode rather than user mode
			 setCheckBox();
			break;
		case 'light':	 // if user pref is light and system pref has changed to light, need to swap the prefs
			if (window.matchMedia('(prefers-color-scheme: light)').matches) {
				swapDarkLight();
			}
			break;
		case 'dark': 	// if user pref is dark and system pref has changed to dark, need to swap the prefs
			if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
				swapDarkLight();
			}
			break;
	}
	// force page refresh, otherwise the correct mode may not be displayed
	location.reload();
}

 

The next function is the function that actually changes the user mode between light and dark. First it has to detect the current mode, sets the global variable “Gtheme” accordingly, and sets the Reset Button to enabled. Then it swaps the prefers-color-scheme rules if required and stores the current theme in the Local Storage. Note that the Demo page with reset message includes additional code within the switchTheme function which gives a message to the user about reverting to system preference mode.

function switchTheme(e) {
	try {
		// Get the current theme
		switch (Gtheme) {
			case 'system':
				// get system dark mode setting
				if (!window.matchMedia) {
				 // if matchMedia method is not supported, the default is light mode, so we switch to dark.
				 Gtheme = 'dark';
				} else {
				 if (window.matchMedia('(prefers-color-scheme: light)').matches) {
					// if the system is currently set to light, we override to make it dark
					Gtheme = 'dark';
					swapDarkLight();
					localStorage.setItem('LStheme', 'dark');
				 } else {
					// If we are here, the system is currently set to dark, we override to make it light
					Gtheme = 'light';
					swapDarkLight();
					localStorage.setItem('LStheme', 'light');
				 }
				}
				break;
			case 'light': // change to dark if it is in light mode
				Gtheme = 'dark';
				swapDarkLight();
				localStorage.setItem('LStheme', 'dark');
				break;
			case 'dark': // change to light if it is in dark mode
				Gtheme = 'light';
				swapDarkLight();
				localStorage.setItem('LStheme', 'light');
				break;
			default: // revert to original
				Gtheme = 'system';
				if (GoverridesDL) {swapDarkLight();}
		}
	} catch (e) {
			// console.log(e + ' line: ' + e.lineNumber + ' File: ' + e.fileName);
	}
	
	resetThemeButton();
}

 

Next we have the function that actually swaps the prefers-color-scheme rules from dark to light, or vice-versa - this does not change the actual CSS source files, only the CSS in the browser memory. Note that we have to use a temporary holder (toLight*nWLo%Hy6U) for the dark to light, as otherwise the following line to replace “light” with “dark” would pick up the rules already converted to “light”.

function swapDarkLight(){
	try{
		for(const element of GdarkLightRules) {
			element.media.mediaText = element.media.mediaText.replace('dark', 'toLight*nWLo%Hy6U');
			element.media.mediaText = element.media.mediaText.replace('light', 'dark');
			element.media.mediaText = element.media.mediaText.replace('toLight*nWLo%Hy6U', 'light');
		}
		GoverridesDL = !GoverridesDL;
	} catch (e) {
			// console.log(e + ' line: ' + e.lineNumber + ' File: ' + e.fileName);
	}
}

 

Finally, we have the code that examines all the CSS and finds all the prefers-color-scheme rules and stores them in an array. Note that this will also pick up prefers-color-scheme rules within a <style> tag in the HTML document.

function* visitCssRule(cssRule) {
	try{
		// visit imported stylesheet
		if (cssRule.type == cssRule.IMPORT_RULE) {
			yield* visitStyleSheet(cssRule.styleSheet);
		}
		// yield media rule
		if (cssRule.type == cssRule.MEDIA_RULE) {
			// See if it is a prefers-color-scheme rule, ignore all whitespace
			if (removeWhiteSpace(cssRule.media.mediaText) === '(prefers-color-scheme:dark)' || removeWhiteSpace(cssRule.media.mediaText) === '(prefers-color-scheme:light)' ){
				yield cssRule;
			}
		}
	} catch (e) {
			// console.log(e + ' line: ' + e.lineNumber + ' File: ' + e.fileName);
	}	
}

function* visitStyleSheet(styleSheet) {
	try {
		// visit every rule in the stylesheet
		let cssRules = styleSheet.cssRules;
		for (let i = 0, cssRule; cssRule = cssRules[i]; i++) {
			yield* visitCssRule(cssRule);
		}
	} catch (e) {
			// console.log(e + ' line: ' + e.lineNumber + ' File: ' + e.fileName);
	}
}

function* findDarkLightRules() {
	try{
		// visit all stylesheets
		let styleSheets = document.styleSheets;
		for (let i = 0, styleSheet; styleSheet = styleSheets[i]; i++) {
			yield* visitStyleSheet(styleSheet);
		}
	} catch (e) {
			// console.log(e + ' line: ' + e.lineNumber + ' File: ' + e.fileName);
	}
}

function removeWhiteSpace (myString) {
	return myString.replace(/\s/g, '');
}

 

2-way switch vs 3-way switch

This implementation of changing Dark/Light mode uses a check-box as a 2-way switch which renders every page on the site either dark or light, regardless of the system setting. I also provide a separate option to revert to the system setting. A reader has suggested (see the comments) that a 3-way switch is preferable, giving the three options of always dark, always light, or follow system setting.

 

However, the vast majority of users use a fixed system setting of either light or dark, which means that these users won’t see any difference when the 2-way switch is set to dark and their system setting is also set to dark, or when the 2-way switch is set to light and their system setting is also set to light. The afore-mentioned reader argued that some users might have their system setting set to Light mode at some times of the day, and to Dark mode at other times. However, I think that the simplicity and compactness of the 2-way switch is preferable for the vast majority of users, and providing there is an option to revert to system setting then every user is catered for. On my own site I have now implemented a message that reminds users that there is an option to reset to system settings and which will only appear the first two times the Dark/Light mode is changed. It requires a HTML element that has the id of DarkMsg and which contains the message; the JavaScript switchTheme function is modified and also includes a new function fadeOutEffect. If you are interested in implementing this, the demo and files can be seen at Simple Dark mode Demo page with Reset message.

 

 

Finally…

Finally, as noted above, you can see a demo of the essential files at Basic Dark mode Demo page and the links for downloads are above (Download links).

 

If you have any comments, suggestions, or questions about this Dark mode method, or require help in implementing the method, please feel free to contact me.

Interested in supporting this site?

You can help by sharing the site with others. You can also donate at Go Get Funding: Logic and Language where there are full details.

 

 

As site owner I reserve the right to keep my comments sections as I deem appropriate. I do not use that right to unfairly censor valid criticism. My reasons for deleting or editing comments do not include deleting a comment because it disagrees with what is on my website. Reasons for exclusion include:
Frivolous, irrelevant comments.
Comments devoid of logical basis.
Derogatory comments.
Long-winded comments.
Comments with excessive number of different points.
Questions about matters that do not relate to the page they post on. Such posts are not comments.
Comments with a substantial amount of mathematical terms not properly formatted will not be published unless a file (such as doc, tex, pdf) is simultaneously emailed to me, and where the mathematical terms are correctly formatted.


Reasons for deleting comments of certain users:
Bulk posting of comments in a short space of time, often on several different pages, and which are not simply part of an ongoing discussion. Multiple anonymous user names for one person.
Users, who, when shown their point is wrong, immediately claim that they just wrote it incorrectly and rewrite it again - still erroneously, or else attack something else on my site - erroneously. After the first few instances, further posts are deleted.
Users who make persistent erroneous attacks in a scatter-gun attempt to try to find some error in what I write on this site. After the first few instances, further posts are deleted.


Difficulties in understanding the site content are usually best addressed by contacting me by e-mail.

 

Based on HashOver Comment System by Jacob Barkdull

Copyright   James R Meyer   2012 - 2025
https://jamesrmeyer.com