Load the menuLoad the menu
Logic and
Language


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

How to Create a Great Dark Mode Switch

  Completely new version 09 Sep 2025

 

What are the biggest hassles in having a Light/Dark mode switch on your website? Until recently, the hassles all revolve around the fact that in order to implement both Light and Dark modes for a website, you need to have two separate locations for any class or id where you write the declarations for that class or id; one for the main definition for that id or class, and another for the different definition pertaining to Dark mode (for convenience, the assumption here is that Light mode is the default, but the method is the same for Dark mode as the default). Wouldn’t it be so much easier if the definitions for both Light and Dark modes were within a single overall declaration?

 

This is where a relatively recent addition to CSS comes in - there is now a property color-scheme and a property light-dark and these two properties allow you to specify two different values for a color in the same line, one for Light mode, and one for Dark mode. This makes it much easier to add, edit and maintain colors for both Light and Dark modes. This is the underlying basis for the method described here for implementing a Dark/Light switch for a website.

 

You should be aware that since this is a fairly recent addition to CSS rules, there are still some browsers out there that will not support these properties; you can see the current browser support at light-dark browser support and color-scheme browser support, and you can decide whether this is a viable option for your website. You might be able to have a safe fall-back by including a default single color declaration before the two color declaration, so that even if the two color declaration is not recognized by the browser, the single color declaration will apply. This possibility is not covered here.

 

While this method is a great step forward, there is a minor snag in that it only applies to colors, so that if you want to some other non-color property to change when the Light/Dark mode changes you still have to use a separate prefers-color-scheme declaration; for example, if you want to invert the colors of an image you need to have the CSS filter: invert(1) to apply in one mode and not in the other, and you have to put this within a prefers-color-scheme declaration. Note that for some features you can set one color to transparent, and that will prevent the user seeing that feature, but you need to be aware that for some features such as borders, the transparent feature still occupies space on the screen. But since the vast majority of the changes between modes will be for colors, the quantity of such prefers-color-scheme declarations will be small and easily manageable. That said, we still have to deal with them if we are creating a Light/Dark switch. I describe how this is done later, but first a little information about how Dark and Light modes operate.

 

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 being in Dark mode, or the browser may set its system to Dark or Light 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”).

 

The new color-scheme and light-dark properties operate as follows: the light-dark is a function that returns a color, e.g:

.someClassName {
	background: light-dark(white, hsl(52, 36%, 9%));
}

where the first color will be shown when Light mode is active, and the second when Dark mode is active. You can use variables to hold colors that will be used in several places, for example:

:root {
	--primary-color: light-dark(#14172b, #f6f4ec);
	--secondary-color: light-dark(#f6f4ec, #14172b);
	--text-color: light-dark(#2e2590, #e1e6db);
	--bg-color: light-dark(#dbe9c2, #7a1522);
}

so that you only need to refer to the variable, e.g:

.someClassName{
	color: var(--primary-color);
}

Note that only actual color definitions can be used within the light-dark()function, you cannot use terms such as inherit, initial, revert, unset. To enable the light-dark() color function, the color-scheme must be defined to have a value of light, dark or light dark. For example, this can be done by declaring properties that can be applied to the webpage, e.g:

:root {
	color-scheme: dark;
}

With this method, to create a switch that enables the user to change the page to the desired Dark or Light mode without changing the overall system setting simply involves changing the color-scheme that applies to the page.

 

The other, traditional method for Light/Dark is to have the main CSS code define the values for the default mode (usually Light) and then have a separate media query section where the non-default values are defined, e.g:

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

so that if the user has set the system mode as “dark”, then the page will display according to the code within this @media section. This 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.

 

With this method, to create a switch that enables the user to change the page to the desired Dark or Light mode without changing the overall system setting is somewhat more involved. The conventional approach is to apply a class or style to the web-page that defines the same code as within the @media (prefers‑color‑scheme: dark) section, but the method used here is somewhat different. I will explain it by an example:

@media (prefers-color-scheme: dark) {
	.someClassName {border-top : 2px solid red;}
}

If the the main CSS code does not define a border for Light mode for the class someClassName, then if the system preference is set to Light no border appears, but the border will appear if the system preference is set to Dark. 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: Note that the CSS file is not changed, only the information held in memory.

@media (prefers-color-scheme: light) {
	.someClassName {border-top : 2px solid red;}
}

This means that if the system preference is set to Light mode and the user clicks the switch to Dark mode, 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. If JavaScript is disabled or non functional for any reason, it defaults to the system preference. 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. You simply write and maintain your CSS code as if you were simply writing your web-page to follow the system's Light/Dark mode setting, you don't have to worry about anything else.

 

To implement my Dark/Light switch, you don’t need to know in detail how the JavaScript works, you can simply add the appropriate code inside the top of the 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".

 

If you want to read in more detail about the light-dark function and related matters, there is a good article at Come to the light-dark() Side.

 

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 to the user how to revert to following the system setting as Dark or Light mode, you can download copies of these demo files here:

 

The Code In the Head

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.

 

We want to avoid a flash of light mode before the desired Dark mode loads or is refreshed (or a flash of Dark mode before the desired light mode loads or is refreshed). The way we do this is by:

  1. adding a small <style> section before loading any CSS or JavaScript files, which adds a CSS variable to the root of the webpage, and which also defines that the color-scheme of the page has the value of this variable; the default value is to follow the system Light or Dark mode.
  2. adding a small <script> section that sets the value of this variable mode according to the browser’s Local Storage.

Since the CSS code is applied immediately to the document before anything is visible to the user, the correct color-scheme is applied immediately so that there is no flash of unwanted Light or Dark (this assumes that the bulk of the color changes is executed by the light-dark() function method). There is no need to worry about a hit on performance as the amount of code required is very small. The initial code in the head adds the variable and sets the color-scheme to light dark : The <meta name="color-scheme" content="light dark"> is not essential, but it is recommended, see MDN Web Docs: color-scheme.

<meta name="color-scheme" content="light dark">
<style>:root {
	--DarkLight:light dark;
	color-scheme: var(--DarkLight);
	}
</style>

followed by a small script section:

<script>
let Gtheme = "system";
let GoverridesDL = false;
switch ((Gtheme = (Gtheme = localStorage.getItem('LStheme')) || 'system')) {
	case 'system':
		break;
	case 'light':
		document.documentElement.style.setProperty('--DarkLight', 'light');
		if (window.matchMedia('(prefers-color-scheme: dark)').matches){
			GoverridesDL = !GoverridesDL;
		}
		break;
	case 'dark':
		document.documentElement.style.setProperty('--DarkLight', 'dark');
		if (window.matchMedia('(prefers-color-scheme: light)').matches){
			GoverridesDL = !GoverridesDL;
		}
	}	
</script>

and the minified versions are:

<meta name="color-scheme" content="light dark">
<style>:root{--DarkLight:light dark;color-scheme: var(--DarkLight)}</style>

and:

<script>let Gtheme="system",GoverridesDL=!1;switch(Gtheme=(Gtheme=localStorage.getItem("LStheme"))||"system"){case"system":break;case"light":document.documentElement.style.setProperty("--DarkLight","light"),window.matchMedia("(prefers-color-scheme: dark)").matches&&(GoverridesDL=!GoverridesDL);break;case"dark":document.documentElement.style.setProperty("--DarkLight","dark"),window.matchMedia("(prefers-color-scheme: light)").matches&&(GoverridesDL=!GoverridesDL)}</script>

 

The Main JavaScript Code

In the following the JavaScript file for implementing the initial setup is described. This link to 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());
	
	// Swap the prefers-color-scheme dark/light if already indicated by head code
	if (GoverridesDL) {swapPrefers();}

		// 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 {
		// 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 to reset the mode to the system default
function resetTheme() {
	try {
		// if current system pref is light and current Gtheme is dark, 
		// or if if current system pref is dark and current Gtheme is light, need to swap the prefs
		if ((window.matchMedia('(prefers-color-scheme: light)').matches && Gtheme === 'dark') ||
		(window.matchMedia('(prefers-color-scheme: dark)').matches && Gtheme === 'light')){
			swapPrefers();
		}
		document.documentElement.style.setProperty('--DarkLight', 'light dark');
	} 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) {
				swapPrefers();
			}
			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) {
				swapPrefers();
			}
			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.

// DARK & LIGHT THEME SWITCHER
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';
					swapPrefers();
					document.documentElement.style.setProperty('--DarkLight', 'dark');
					localStorage.setItem('LStheme', 'dark');
				 } else {
					// If we are here, the system is currently set to dark, we override to make it light
					Gtheme = 'light';
					swapPrefers();
					document.documentElement.style.setProperty('--DarkLight', 'light');
					localStorage.setItem('LStheme', 'light');
				 }
				}
				break;
			case 'light': // change to dark if it is in light mode
				Gtheme = 'dark';
				swapPrefers();
				document.documentElement.style.setProperty('--DarkLight', 'dark');
				localStorage.setItem('LStheme', 'dark');
				break;
			case 'dark': // change to light if it is in dark mode
				Gtheme = 'light';
				swapPrefers();
				document.documentElement.style.setProperty('--DarkLight', 'light');
				localStorage.setItem('LStheme', 'light');
				break;
			default: // revert to original
				Gtheme = 'system';
				document.documentElement.style.setProperty('--DarkLight', 'light dark');
				if (GoverridesDL) {swapPrefers();}
		}
		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 swapPrefers(){
	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).

 

Note that you should check how your web-pages look in print preview if you click print when the page is set to dark mode, because there could be unintended effects.

 

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