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:
- 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 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/
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:
- Download the HTML code: dark-mode-demo.html
- Download the basic CSS: dark-mode-demo.css
- Download the JavaScript code: dark-mode-demo.js
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
- Download the HTML code: dark-mode-demo-with-msg.html
- Download the basic CSS: dark-mode-demo-with-msg.css
- Download the JavaScript code: dark-mode-demo-with-msg.js
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:
- 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 thecolor-scheme
of the page has the value of this variable; the default value is to follow the system Light or Dark mode. - 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/
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).
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.
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.