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/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/
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/
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/
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:
- 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..
- It requires no modifications of the CSS code, with no additional
ids
orclasses
. - You can have as many
prefers-color-scheme: dark
andprefers-color-scheme: light
sections as you want in your CSS files and in<style>
tags in your web-page document. You can even include bothprefers-color-scheme: dark
andprefers-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 theprefers-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. - It defaults to the system preference if JavaScript is disabled or non functional for any reason.
- 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.
- 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:
- where the user has selected the overall browser/
system to be in Dark mode, and - 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/
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:
- Download the HTML code: dark-mode-demo.html
- Download the basic CSS: dark-mode-demo-2.css
- Download the JavaScript code: dark-mode-demo-2.js
And there are also demo files that include a message explaining how to revert to the system Dark/
- Download the HTML code: dark-mode-demo-with-msg.html
- Download the basic CSS: dark-mode-demo-with-msg-2.css
- Download the JavaScript code: dark-mode-demo-with-msg-2.js
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/
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/
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/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.
Rationale: Every logical argument must be defined in some language, and every language has limitations. Attempting to construct a logical argument while ignoring how the limitations of language might affect that argument is a bizarre approach. The correct acknowledgment of the interactions of logic and language explains almost all of the paradoxes, and resolves almost all of the contradictions, conundrums, and contentious issues in modern philosophy and mathematics.
Site Mission
Please see the menu for numerous articles of interest. Please leave a comment or send an email if you are interested in the material on this site.
Interested in supporting this site?
You can help by sharing the site with others. You can also donate at
where there are full details.