Reputation: 4698
I am implementing a dark mode, as macOS, Windows and iOS have all introduced dark modes.
There is a native option for Safari, Chrome, and Firefox, using the following CSS media rule:
@media (prefers-color-scheme: dark) {
body {
color:#fff;
background:#333333
}
This will automatically identify systems that are set to dark modes, and apply the enclosed CSS rules.
However; even though users may have their system set to dark mode, it may be the case that they prefer the light or default theme of a specific website. There is also the case of Microsoft Edge users which does not (yet) support @media (prefers-color-scheme
. For the best user experience, I want to ensure that these users can toggle between dark and default modes for those cases.
Is there a method that this can be performed, possibly with HTML 5 or JavaScript? I'd include the code I have tried, but I haven't been able to find any information on implementing this whatsoever!
Upvotes: 108
Views: 120863
Reputation: 1166
Here's an answer that respects the default prefers-color-scheme
, and only then lets you toggle via localStorage
. This assumes CSS does it faster than JS plus people will use the default scheme even without JS.
I don't like having to declare a default style and then re-declaring it as a standalone class called, but it's unavoidable. I at least used :root
to avoid duplicated values.
Note this forum seems to block localStorage
so you have to try the code somewhere else.
var theme, prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
if (prefersDarkScheme.matches)
theme = document.body.classList.contains("light-mode") ? "light" : "dark";
else
theme = document.body.classList.contains("dark-mode") ? "dark" : "light";
localStorage.setItem("theme", theme);
function toggle() {
var currentTheme = localStorage.getItem("theme");
if (currentTheme == "dark")
document.body.classList.toggle("light-mode");
else if (currentTheme == "light")
document.body.classList.toggle("dark-mode");
}
:root {
--text-for-light: black;
--bkg-for-light: white;
--link-for-light: blue;
--text-for-dark: white;
--bkg-for-dark: black;
--link-for-dark: DeepSkyBlue;
}
body {color: var(--text-for-light); background-color: var(--bkg-for-light);}
a {color: var(--link-for-light);}
.dark-mode {color: var(--text-for-dark); background-color: var(--bkg-for-dark);}
.dark-mode a {color: var(--link-for-dark);}
.light-mode {color: var(--text-for-light); background-color: var(--bkg-for-light);}
.light-mode a {color: var(--link-for-light);}
@media (prefers-color-scheme: dark) {
body {color: var(--text-for-dark); background-color: var(--bkg-for-dark);}
a {color: var(--link-for-dark);}
}
<button onclick="toggle()">Toggle Light/Dark Mode</button>
<p> </p>
Test <a href="link">link</a>
If you want just the auto detect part without the toggle button:
:root {
--text-for-light: black;
--bkg-for-light: white;
--link-for-light: blue;
--text-for-dark: white;
--bkg-for-dark: black;
--link-for-dark: DeepSkyBlue;
}
body {color: var(--text-for-light); background-color: var(--bkg-for-light);}
a {color: var(--link-for-light);}
@media (prefers-color-scheme: dark) {
body {color: var(--text-for-dark); background-color: var(--bkg-for-dark);}
a {color: var(--link-for-dark);}
}
Test <a href="link">link</a>
Upvotes: 6
Reputation: 5439
One issue I faced with the original solution was that the changes to the color scheme were not affecting the scrollbar color. However, this issue can be resolved by using the color-scheme
CSS property in conjunction with the :root
pseudo-element.
(As of IE end of life, August 17th 2021 🥳✌️🎉)
In short, the idea is to store the color scheme in the browser's local storage and update the color scheme when the user's preference are changed, without reloading the page. If there is no stored color scheme, the system preference is applied.
We get the stored color scheme from the browser's local storage using window.localStorage.getItem("scheme")
. This value will be null
if there is no stored value. If there is no stored value, we checks if the user's device/browser prefers a dark color scheme using window.matchMedia("(prefers-color-scheme: dark)").matches
.
(function(){
// Get the toggle element by its ID
var e = document.getElementById("tglScheme");
// If the toggle element is not found, log an error and exit the function
if (!e) { console.error('No element with ID "tglScheme" found. Unable to handle color scheme toggle.'); return; }
// Check if the user's device/browser prefers a dark color scheme
var t = window.matchMedia("(prefers-color-scheme: dark)").matches;
// Get the stored color scheme from the browser's local storage
var n = window.localStorage.getItem("scheme");
// Function to apply the color scheme based on the given scheme value
var i = function(r){
var s = document.getElementById("scheme"); s && s.remove(); document.head.insertAdjacentHTML("beforeend",'<style id="scheme">:root{color-scheme:'+r+"}</style>");document.body.classList.remove("light","dark");document.body.classList.add(r);e.checked="dark"===r;console.log("Color scheme applied:",r);
};
// Determine the initial color scheme based on user preference or stored value
var a = n || (t ? "dark" : "light"); i(a);
// Attach a click event listener to the toggle element
e.addEventListener("click", function(){
// Check if the toggle is checked or not
var r = e.checked;
// Apply the new color scheme based on the toggle state
var o = r ? "dark" : "light"; i(o);
// Update the stored color scheme in the browser's local storage
r ? (localStorage.setItem("scheme", "dark"), console.log("User preference saved: dark")) : (localStorage.setItem("scheme", "light"), console.log("User preference saved: light"));
});
// Update the color scheme when the user's preference changes, without reloading the page
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", function(){
// Update the stored color scheme from the browser's local storage
n = window.localStorage.getItem("scheme");
// If there's no stored color scheme, apply the system preference
if (!n) { t = window.matchMedia("(prefers-color-scheme: dark)").matches; var r = t ? "dark" : "light"; i(r); console.log("System color scheme preference changed:", r); }
});
})();
/* Default theme variables */
:root {
--app-bg: #ffffff;
--app-tx: #000000;
}
/* Dark theme variables */
body.dark {
--app-bg: #131313;
--app-tx: #f8f9fa;
}
/* Apply the background and text colors using the variables */
body {
background-color: var(--app-bg);
color: var(--app-tx);
}
<label for="tglScheme">Toggle Dark Mode:</label>
<input type="checkbox" id="tglScheme" />
Visit: https://codepen.io/amarinediary/pen/yLgppWW to see it live on CodePen.
Upvotes: 5
Reputation: 73
Took the solution provided by @JimmyBanks and 1) turned the checkbox into a toggling text button, and 2) added automatic theme switching on OS theme change.
CSS is unchanged, with light themes stored in the :root
and dark themes stored under [data-theme="dark"]
:
The <head>
JS has some edits, including a few omissions and the move of the data-theme
statement to the subsequent JS block:
Please let me know if you have any suggestions for improvement!
var theme = 'light';
if (localStorage.getItem('theme')) {
if (localStorage.getItem('theme') === 'dark') {
theme = 'dark';
}
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
theme = 'dark';
}
/*
And here is the edit to the second block of JS, plus related HTML. `theme_switch` toggles the theme, while `theme_OS` automatically updates the site's theme with changes to the OS theme.
*/
var theme;
function theme_apply() {
'use strict';
if (theme === 'light') {
document.getElementById('theme_readout').innerHTML = 'Dark';
document.documentElement.setAttribute('data-theme', 'light');
localStorage.setItem('theme', 'light');
} else {
document.getElementById('theme_readout').innerHTML = 'Light';
document.documentElement.setAttribute('data-theme', 'dark');
localStorage.setItem('theme', 'dark');
}
}
theme_apply();
function theme_switch() {
'use strict';
if (theme === 'light') {
theme = 'dark';
} else {
theme = 'light';
}
theme_apply();
}
var theme_OS = window.matchMedia('(prefers-color-scheme: light)');
theme_OS.addEventListener('change', function (e) {
'use strict';
if (e.matches) {
theme = 'light';
} else {
theme = 'dark';
}
theme_apply();
});
:root {
--color_01: #000;
--color_02: #fff;
--color_03: #888;
}
[data-theme="dark"] {
--color_01: #fff;
--color_02: #000;
--color_03: #777;
}
<a onclick="theme_switch()">Theme: <span id="theme_readout"></span></a>
Upvotes: 5
Reputation: 8775
My answer is based on this one, but I have included changes I had to make to get it working, plus I added the ability to persist in local storage.
The important point is that it works with @media (prefers-color-scheme: dark)
CSS, and there was no need to create or duplicate additional CSS classes just for this to work. In other words it works with native CSS color scheme.
On the page I first added a sun/moon icon and set them to invisible.
So on page load, the applyPreferredColorScheme(getPreferredColorScheme())
method runs, which checks the system and local storage and figures out which theme to apply. It also switches between the sun or moon icon depending on the current theme.
When the user clicks the icon to toggle the theme, toggleColorScheme()
runs which stores the chosen theme in local storage, but one difference - if the user switches back to the theme that matches their OS, the code simply removes the item from local storage. Trying to keep it as native as possible.
JSFiddle: https://jsfiddle.net/35e0a97a/xmt1k659/78/
// https://stackoverflow.com/questions/56300132/how-to-override-css-prefers-color-scheme-setting
// Return the system level color scheme, but if something's in local storage, return that
// Unless the system scheme matches the the stored scheme, in which case... remove from local storage
function getPreferredColorScheme(){
let systemScheme = 'light';
if(window.matchMedia('(prefers-color-scheme: dark)').matches){
systemScheme = 'dark';
}
let chosenScheme = systemScheme;
if(localStorage.getItem("scheme")){
chosenScheme = localStorage.getItem("scheme");
}
if(systemScheme === chosenScheme){
localStorage.removeItem("scheme");
}
return chosenScheme;
}
// Write chosen color scheme to local storage
// Unless the system scheme matches the the stored scheme, in which case... remove from local storage
function savePreferredColorScheme(scheme){
let systemScheme = 'light';
if(window.matchMedia('(prefers-color-scheme: dark)').matches){
systemScheme = 'dark';
}
if(systemScheme === scheme){
localStorage.removeItem("scheme");
}
else {
localStorage.setItem("scheme", scheme);
}
}
// Get the current scheme, and apply the opposite
function toggleColorScheme(){
let newScheme = "light";
let scheme = getPreferredColorScheme();
if (scheme === "light"){
newScheme = "dark";
}
applyPreferredColorScheme(newScheme);
savePreferredColorScheme(newScheme);
}
// Apply the chosen color scheme by traversing stylesheet rules, and applying a medium.
function applyPreferredColorScheme(scheme) {
for (var s = 0; s < document.styleSheets.length; s++) {
for (var i = 0; i < document.styleSheets[s].cssRules.length; i++) {
rule = document.styleSheets[s].cssRules[i];
if (rule && rule.media && rule.media.mediaText.includes("prefers-color-scheme")) {
switch (scheme) {
case "light":
rule.media.appendMedium("original-prefers-color-scheme");
if (rule.media.mediaText.includes("light")) rule.media.deleteMedium("(prefers-color-scheme: light)");
if (rule.media.mediaText.includes("dark")) rule.media.deleteMedium("(prefers-color-scheme: dark)");
break;
case "dark":
rule.media.appendMedium("(prefers-color-scheme: light)");
rule.media.appendMedium("(prefers-color-scheme: dark)");
if (rule.media.mediaText.includes("original")) rule.media.deleteMedium("original-prefers-color-scheme");
break;
default:
rule.media.appendMedium("(prefers-color-scheme: dark)");
if (rule.media.mediaText.includes("light")) rule.media.deleteMedium("(prefers-color-scheme: light)");
if (rule.media.mediaText.includes("original")) rule.media.deleteMedium("original-prefers-color-scheme");
break;
}
}
}
}
// Change the toggle button to be the opposite of the current scheme
if (scheme === "dark") {
document.getElementById("icon-sun").style.display = 'inline';
document.getElementById("icon-moon").style.display = 'none';
} else {
document.getElementById("icon-moon").style.display = 'inline';
document.getElementById("icon-sun").style.display = 'none';
}
}
applyPreferredColorScheme(getPreferredColorScheme());
#icon-sun {
width: 1.5rem;
height: 1.5rem;
display: none;
}
#icon-moon {
width: 1.5rem;
height: 1.5rem;
display: none;
}
<a href="javascript:toggleColorScheme();">
<span id="icon-sun">🌞</span>
<span id="icon-moon">🌚</span>
</a>
Upvotes: 11
Reputation: 920
https://codepen.io/glorious73/pen/rNRXGex
The following is a solution that uses the media query which checks if the users prefers dark theme and sets the theme as such. Then it allows the user to switch themes and saves it in local storage for future visits.
window.matchMedia("(prefers-color-scheme: dark)")
.html
and then save it in local storage.The source code below should serve for a good example.
html
<!DOCTYPE html>
<!--To set the theme for the html element below-->
<html>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/img/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
</head>
<body>
<div id="main">
<h1>This is a dummy text illustrating the use of themes.</h1>
<div class="entry">
<h2 class="key">Theme</h2>
<div class="radios">
<input type="radio" id="light" name="theme">
<label class="option-one" for="light">
Light
</label>
<input type="radio" id="dark" name="theme">
<label class="option-two" for="dark">
Dark
</label>
</div>
</div>
</div>
<script type="module" src="/src/index.js"></script>
</body>
</html>
Now, in index.js
, here is what needs to be done to set the theme.
Javascript
class Theme {
constructor() {
this.selectedTheme = localStorage.getItem("theme"); // 'light' or 'dark'
}
static getInstance() {
if (!this.instance)
this.instance = new Theme();
return this.instance;
}
setTheme(theme) {
this.selectedTheme = theme;
const html = document.querySelector("html");
html.setAttribute("theme", theme); // set theme here!!
localStorage.setItem("theme", theme); // future visits
}
getTheme() {
return this.selectedTheme;
}
}
function loadTheme() {
let selectedTheme = Theme.getInstance().getTheme();
if(!selectedTheme) {
const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
selectedTheme = (prefersDarkScheme.matches) ? "dark" : "light";
}
Theme.getInstance().setTheme(selectedTheme);
}
loadTheme()
Now, the styles in your app should solely depend on variables which change based on the theme
attribute of the html
element, like so.
CSS
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
:root {
--primary-color: #ffffff;
--secondary-color: #2d5c8a;
--accent-color: #f2f2f2;
--text-color: #1b1212;
--white-color: #ffffff;
--success-color: #077d0b;
--error-color: #ff4d00;
--box-shadow-color: rgba(0,0,0,0.2);
color-scheme: light dark;
}
html[theme="dark"] {
--primary-color: #242424;
--secondary-color: #2d5c8a;
--accent-color: #1f1f1f;
--text-color: #ffffff;
--white-color: #ffffff;
--success-color: #077d0b;
--error-color: #ff4d00;
--box-shadow-color: rgba(212, 212, 212, 0.2);
}
body {
background-color: var(--primary-color);
color: var(--text-color);
}
Finally, all you need is a theme switcher to change the theme while the user is using the app. The following is an example that shows you how one can be done.
<div class="entry">
<h2 class="key">Theme</h2>
<div class="radios">
<input type="radio" id="light" name="theme">
<label class="option-one" for="light">
Light
</label>
<input type="radio" id="dark" name="theme">
<label class="option-two" for="dark">
Dark
</label>
</div>
</div>
.key {
font-weight: normal;
}
.entry {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 1rem;
}
.radios {
display: flex;
align-items: center;
border: 1px solid var(--text-color);
border-radius: 0.8rem;
width: 10rem;
padding: 0.5rem 0;
padding: 0;
}
input[type=radio] {
display: none;
}
input[type="radio" i] {
margin: 0 !important;
padding: 0 !important;
}
input[type=radio]+label {
display: inline-block;
width: 5rem;
padding: 0.5rem 0;
font-size: 1rem;
text-align: center;
cursor: pointer;
background-color: var(--primary-color);
color: var(--text-color);
transition: all 0.25s ease;
}
input[type=radio]+label.option-one {
border-top-left-radius: 0.8rem;
border-bottom-left-radius: 0.8rem;
}
input[type=radio]+label.option-two {
border-top-right-radius: 0.8rem;
border-bottom-right-radius: 0.8rem;
}
input[type=radio]:hover+label {
font-weight: bold;
}
input[type=radio]:checked+label {
outline: 0;
color: var(--primary-color) !important;
background: var(--secondary-color);
}
const currentTheme = Theme.getInstance().getTheme();
const themes = document.querySelectorAll('input[name="theme"]');
themes.forEach(element => {
if(element.id == currentTheme)
element.setAttribute("checked", "checked");
element.addEventListener("change", async () => await setTheme(element.id));
});
function setTheme(theme) {
Theme.getInstance().setTheme(theme);
}
Upvotes: 1
Reputation: 194
Modifying the stylesheets is unreliable so here's another solution.
@media (prefers-color-scheme: light) {
html {
filter: none;
}
}
@media (prefers-color-scheme: dark) {
html {
filter: invert(100%);
}
}
export function forceColorScheme(scheme: "light" | "dark" | null) {
const htmlElement = document.querySelector(":root") as HTMLHtmlElement;
switch (scheme) {
case "light":
htmlElement.style.filter = "none";
break;
case "dark":
htmlElement.style.filter = "invert(100%)";
break;
default:
htmlElement.style.removeProperty("filter");
break;
}
}
Depending on your use case, it may be better to replace class names instead.
Upvotes: 0
Reputation: 3863
Based on @some1and2 answer, here is a React solution using usehooks-ts
:
import {
FC,
useEffect,
} from 'react';
import {
useMediaQuery,
useTernaryDarkMode,
} from 'usehooks-ts';
export const ThemeSwitch: FC = () => {
const isSystemDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
const {
isDarkMode,
ternaryDarkMode,
setTernaryDarkMode,
toggleTernaryDarkMode,
} = useTernaryDarkMode()
type TernaryDarkMode = typeof ternaryDarkMode
useEffect(() => {
for (let s = 0; s < document.styleSheets.length; s++) {
const styleSheet = document.styleSheets.item(s);
const cssRules = styleSheet?.cssRules || new CSSRuleList();
for (let r = 0; r < cssRules.length; r++) {
const cssRule = cssRules.item(r);
if (!(cssRule instanceof CSSMediaRule)) {
continue;
}
const ruleMediaText = cssRule.media.mediaText;
if (!ruleMediaText.match(/(prefers-color-scheme: \w+)/)) {
continue;
}
/**
* Replace the prefers-color-scheme media query to apply the desired theme.
* It refers to the system media query in order to keep the mechanic in sync by reversing it.
*/
const newRuleMediaText = ruleMediaText.replace(
/(dark|light)/,
isDarkMode
? isSystemDarkMode ? 'dark' : 'light'
: isSystemDarkMode ? 'light' : 'dark'
);
cssRule.media.deleteMedium(ruleMediaText);
cssRule.media.appendMedium(newRuleMediaText);
}
}
}, [isDarkMode, isSystemDarkMode]);
return (
<div>
<p>System theme: {isSystemDarkMode ? 'dark' : 'light'}</p>
<p>Current theme: {isDarkMode ? 'dark' : 'light'}</p>
<p>ternaryMode: {ternaryDarkMode}</p>
<p>
<button onClick={toggleTernaryDarkMode}>
Toggle from {ternaryDarkMode}
</button>
</p>
<p>
Select a mode
<br />
<select
name="dark-mode-select"
onChange={ev =>
setTernaryDarkMode(ev.target.value as TernaryDarkMode)
}
value={ternaryDarkMode}
>
<option value="light">light</option>
<option value="system">system</option>
<option value="dark">dark</option>
</select>
</p>
</div>
);
}
Here some important details to notice:
isSystemDarkMode
variable, representing the system preferred scheme. It is important to reverse the switch mechanic. Otherwise, you will have a light theme being set on dark mode and vice-versa.media
attribute of the link
html element, you will need to play with document.styleSheets[*].media
instead of the rules. You can refer to the GoogleChromeLabs/dark-mode-toggle
project as a reference.Upvotes: 0
Reputation: 149
This solution is loosely based on this solution https://stackoverflow.com/a/75124760/15474643 however actually functions properly (this person doesn't account for cases such as @media (prefers-color-scheme: light) {}
).
/*
JS file for managing light / dark themes
The toggle_theme(); function toggles the saved theme and updates the screen accordingly
The remove_theme(); function removes the theme from localstorage and only updates the screen if it doesn't match the system settings
The window.matchMedia(); function call watches for updates to system settings to keep localstorage settings accurate
*/
function get_system_theme() {
/*
Function for getting the system color scheme
*/
theme = "dark";
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
theme = "light";
}
return theme;
}
function toggle_saved_theme() {
/*
Function for toggling between the two themes saved to local storage
Returns:
Value stored in local storage
*/
// Gets Current Value
if (localStorage.getItem("theme")) {
theme = localStorage.getItem("theme");
}
else {
theme = get_system_theme();
}
// Sets the stored value as the opposite
if (theme === "light") {
localStorage.setItem("theme", "dark");
}
else {
localStorage.setItem("theme", "light");
}
return localStorage.getItem("theme");
}
function switch_theme_rules() {
/*
Function for switching the rules for perfers-color-scheme
Goes through each style sheet file, then each rule within each stylesheet
and looks for any rules that require a prefered colorscheme,
if it finds one that requires light theme then it makes it require dark theme / vise
versa. The idea is that it will feel as though the themes switched even if they haven't.
*/
for (var sheet_file = 0; sheet_file < document.styleSheets.length; sheet_file++) {
try {
for (var sheet_rule = 0; sheet_rule < document.styleSheets[sheet_file].cssRules.length; sheet_rule++) {
rule = document.styleSheets[sheet_file].cssRules[sheet_rule];
if (rule && rule.media && rule.media.mediaText.includes("prefers-color-scheme")) {
rule_media = rule.media.mediaText;
if (rule_media.includes("light")) {
new_rule_media = rule_media.replace("light", "dark");
}
if (rule_media.includes("dark")) {
new_rule_media = rule_media.replace("dark", "light");
}
rule.media.deleteMedium(rule_media);
rule.media.appendMedium(new_rule_media);
}
}
}
catch (e) {
broken_sheet = document.styleSheets[sheet_file].href;
console.warn(broken_sheet + " broke something with theme toggle : " + e);
}
}
}
function toggle_theme() {
/*
Toggles the current theme used
*/
stored_theme = toggle_saved_theme();
switch_theme_rules();
}
function remove_theme() {
/*
Function for removing theme from local storage
*/
if (localStorage.getItem("theme")) {
if (get_system_theme() != localStorage.getItem("theme")) {
switch_theme_rules();
}
localStorage.removeItem("theme");
}
}
window.matchMedia('(prefers-color-scheme: dark)')
/*
This makes it such that if a user changes the theme on their
browser and they have a preferred theme, the page maintains its prefered theme.
*/
.addEventListener("change", event => {
if (localStorage.getItem("theme")) {
switch_theme_rules(); // Switches Theme every time the prefered color gets updated
}
}
)
if (localStorage.getItem("theme")) {
if (get_system_theme() != localStorage.getItem("theme")) {
switch_theme_rules();
}
}
:root {
--main-background: white;
--text-color: black;
}
@media screen and (prefers-color-scheme: dark) {
:root {
--main-background: black;
--text-color: white;
}
}
body {
background-color: var(--main-background);
}
* {
color: var(--text-color);
}
<!DOCTYPE html>
<html lang="en">
<body>
<p>Some Text</p>
<button onclick="toggle_theme();">Change Theme!</button>
<button onclick="remove_theme();">Remove Theme!</button>
</body>
</html>
Hope this helps someone out there!
Upvotes: 8
Reputation: 2026
Using CSS variables, set a default value and an opposite value in a media query. Also set the values in two classes and implement a toggle that toggles these classes when clicked.
By default, automatic mode is used based on the system color scheme. Using the toggle switches to manual mode. It returns to automatic mode after refreshing the page (or removing the class from the html element).
// toggle to switch classes between .light and .dark
// if no class is present (initial state), then assume current state based on system color scheme
// if system color scheme is not supported, then assume current state is light
function toggleDarkMode() {
if (document.documentElement.classList.contains("light")) {
document.documentElement.classList.remove("light")
document.documentElement.classList.add("dark")
} else if (document.documentElement.classList.contains("dark")) {
document.documentElement.classList.remove("dark")
document.documentElement.classList.add("light")
} else {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add("dark")
} else {
document.documentElement.classList.add("light")
}
}
}
/* automatic/manual light mode */
:root, :root.light {
--some-value: black;
--some-other-value: white;
}
/* automatic dark mode */
/* ❗️ keep the rules in sync with the manual dark mode below! */
@media (prefers-color-scheme: dark) {
:root {
--some-value: white;
--some-other-value: black;
}
}
/* manual dark mode
/* ❗️ keep the rules in sync with the automatic dark mode above! */
:root.dark {
--some-value: white;
--some-other-value: black;
}
/* use the variables */
body {
color: var(--some-value);
background-color: var(--some-other-value);
}
<button onClick="toggleDarkMode()">Toggle</button>
<h1>Hello world!</h1>
Upvotes: 32
Reputation: 21
I just did this today and worked with the dark mode preferences. I think this solution is simpler than all the ones I looked at here.
The toggle darkmode button is optional. Local storage is used, works with iPhone dark mode.
Finally, my research was all the posts here. I worked out an "optimized" solution. See what kind of traction you get. I included the SVG to make things easier.
EDIT: I updated the JS function to check for availability of local storage. I also moved the function variables into the function itself other than the toggle used for real-time setting storage. It is slightly longer this way but more correct and still shorter than most the others.
function checkForLocalStorage () {
try {
localStorage.setItem('test', 1)
localStorage.removeItem('test')
return true
} catch (e) { return false }
}
const hasLocalStorage = checkForLocalStorage()
let isThemeDark = null
function toggleDarkMode () {
const isPreferDark = window.matchMedia('(prefers-color-scheme: dark)').matches
const localPref = hasLocalStorage ? localStorage.getItem('isThemeDark') : null
const hasLocalPref = !!localPref
if (isThemeDark === null && hasLocalPref) isThemeDark = localPref === 'dark'
else if (isThemeDark === null && isPreferDark) isThemeDark = true
else if (isThemeDark === null) isThemeDark = false
else isThemeDark = !isThemeDark
const theme = isThemeDark ? 'dark' : 'light'
if (hasLocalStorage) localStorage.setItem('isThemeDark', theme)
document.body.classList[isThemeDark ? 'add' : 'remove']('dark-mode')
}
toggleDarkMode()
body.dark-mode { background: #222; color: #f2f2f2; }
body.dark-mode #darkModeToggle svg { fill: #fff; }
<a href="#" id="darkModeToggle" onclick="toggleDarkMode()">
<svg width="24px" height="24px"><path d="M12,22 C17.5228475,22 22,17.5228475 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,17.5228475 6.4771525,22 12,22 Z M12,20.5 L12,3.5 C16.6944204,3.5 20.5,7.30557963 20.5,12 C20.5,16.6944204 16.6944204,20.5 12,20.5 Z"/></svg>
</a>
<div>Hello World!</div>
Upvotes: 2
Reputation: 1868
Looks like there's plenty of answers here already, but nothing quite matching my needs. I wanted to be able to:
My CSS looks a lot like that in other solutions. For completeness this is something like:
:root {
/* Support light and dark, with light preferred if no user preference */
color-scheme: light dark;
/* default light mode */
--bg-color: #f7f7f7;
--text-color: #222430;
/* dark mode overrides */
--dark-bg-color: #222430;
--dark-text-color: #f7f7f7;
}
.dark-theme {
/* Use dark mode overrides when class is applied */
--bg-color: var(--dark-bg-color);
--text-color: var(--dark-text-color);
}
body {
background-color: var(--bg-color);
color: var(--text-color);
}
There is a checkbox with id 'darkThemeToggle' in the HTML. The below runs on page load. (Written in TypeScript)
function manageTheme() {
const darkThemeCheckbox = document.querySelector("#darkThemeToggle") as HTMLInputElement;
const agentPrefersDarkQuery = matchMedia("(prefers-color-scheme: dark)");
function setTheme(arg?: MediaQueryListEvent | string) {
let chosenTheme = "";
if (typeof arg === "string") {
// If this function is called with a string, then an explict preference has
// been set by the user. Use that theme and save the setting for the future.
chosenTheme = arg;
localStorage.setItem("theme", chosenTheme);
} else {
// Use any saved preference, else check for query param, else any OS preference.
chosenTheme = localStorage.getItem("theme") ||
new URLSearchParams(window.location.search).get("theme") ||
(agentPrefersDarkQuery.matches ? "dark" : "light");
}
if (chosenTheme === "dark") {
document.documentElement.classList.add("dark-theme");
} else {
document.documentElement.classList.remove("dark-theme");
}
// Update the UX to reflect the theme that was ultimately applied.
darkThemeCheckbox.checked = (chosenTheme === "dark");
}
// Whenever the user changes the OS preference, refresh the applied theme.
agentPrefersDarkQuery.onchange = setTheme;
// Note that the 'change' event only fires on user action, (not when set in code), which is
// great, else this might cause an infinite loop with the code setting it in setTheme.
darkThemeCheckbox.addEventListener('change', ev => {
let themeChosen = darkThemeCheckbox.checked ? "dark" : "light";
setTheme(themeChosen);
});
setTheme(); // Run on initial load.
}
document.addEventListener("DOMContentLoaded", manageTheme);
Upvotes: 0
Reputation: 1237
An alternative solution I found using the blog mybyways which is not mentioned anywhere else but works for me. This is useful only when html uses the prefers-color-scheme css media classes.
Unlike other answers, it uses the stylesheets' rules to append the class (as opposed to adding or removing "dark" or "light" from classList)
By default it takes the style of OS setting and overrides it when toggled. I tried from Google Chrome labs but it didn't work out for me.
function setPreferredColorScheme(mode = "dark") {
console.log("changing")
for (var i = document.styleSheets[0].rules.length - 1; i >= 0; i--) {
rule = document.styleSheets[0].rules[i].media;
if (rule.mediaText.includes("prefers-color-scheme")) {
console.log("includes color scheme")
switch (mode) {
case "light":
console.log("light")
rule.appendMedium("original-prefers-color-scheme");
if (rule.mediaText.includes("light")) rule.deleteMedium("(prefers-color-scheme: light)");
if (rule.mediaText.includes("dark")) rule.deleteMedium("(prefers-color-scheme: dark)");
break;
case "dark":
console.log("dark")
rule.appendMedium("(prefers-color-scheme: light)");
rule.appendMedium("(prefers-color-scheme: dark)");
if (rule.mediaText.includes("original")) rule.deleteMedium("original-prefers-color-scheme");
break;
default:
console.log("default")
rule.appendMedium("(prefers-color-scheme: dark)");
if (rule.mediaText.includes("light")) rule.deleteMedium("(prefers-color-scheme: light)");
if (rule.mediaText.includes("original")) rule.deleteMedium("original-prefers-color-scheme");
}
break;
}
}
}
@media (prefers-color-scheme: light) {
:root {
color: pink;
background-color: yellow;
}
}
@media (prefers-color-scheme: dark) {
:root {
color: red;
background-color: blue;
}
}
<body>
<button onClick="setPreferredColorScheme()">
toggle
</button>
</body>
Above is a working example ^
Full source: https://mybyways.com
Upvotes: 18
Reputation: 93
I suggest using SCSS. You can make it more simpler.
/* Dark Mode */
@mixin darkMixin {
body {
color: #fff;
background: #000;
}
}
@media (prefers-color-scheme: dark) {
@include darkMixin;
}
.darkMode {
@include darkMixin;
}
.lightMode {
body {
color: #000;
background: #fff;
}
}
And you can toggle/override using JavaScript. (In this example, I used jQuery to make it look easy)
// dark
$('html').removeClass('lightMode').addClass('darkMode')
// light
$('html').removeClass('darkMode').addClass('lightMode')
If you want to detect, this is the code based on JimmyBanks' one.
function isDarkTheme(){
let theme="light"; //default to light
if (localStorage.getItem("theme")){
if (localStorage.getItem("theme") == "dark")
theme = "dark"
} else if (!window.matchMedia) {
return false
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
theme = "dark"
}
return theme=='dark'
}
To save the current theme, just use localStorage:
localStorage.setItem("theme", 'light')
or
localStorage.setItem("theme", 'dark')
Upvotes: 0
Reputation: 14929
index.html
<!DOCTYPE html>
<html>
<head>
<meta name="color-scheme" content="light dark">
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<h1>Hello world</h1>
<button id="toggle">Toggle</button>
<script type="text/javascript" src="script.js"></script>
</body>
</html>
style.css
.dark-mode {
background-color: black;
color: white;
}
.light-mode {
background-color: white;
color: black;
}
@media (prefers-color-scheme: dark) {
body {
background-color: black;
color: white;
}
}
script.js
/**
* Adopt:
* the theme from the system preferences; or
* the previously stored mode from the `localStorage`
*/
var initialMode = "light";
var prefersColorSchemeDark = window.matchMedia( "(prefers-color-scheme: dark)" );
if ( prefersColorSchemeDark.matches ) {
initialMode = "dark";
}
if( localStorage.getItem("initialMode") == null ) {
localStorage.setItem("initialMode", initialMode);
}
if( localStorage.getItem("currentMode") == null ) {
localStorage.setItem("currentMode", initialMode);
} else {
let currentMode = localStorage.getItem("currentMode");
if ( currentMode == "dark" && currentMode != initialMode ) {
document.body.classList.add("dark-mode");
} else if ( currentMode == "light" && currentMode != initialMode ) {
document.body.classList.add("light-mode");
}
}
/**
* Process the toggle then store to `localStorage`
*/
document.getElementById('toggle').addEventListener("click", function() {
var initialMode = localStorage.getItem("initialMode");
let currentMode = localStorage.getItem("currentMode");
if ( currentMode == "dark" && currentMode == initialMode ) {
document.body.classList.add("light-mode");
localStorage.setItem("currentMode", "light");
} else if ( currentMode == "light" && currentMode == initialMode ) {
document.body.classList.add("dark-mode");
localStorage.setItem("currentMode", "dark");
} else if ( currentMode != initialMode ) {
document.body.removeAttribute("class");
if( currentMode == "dark" ) {
localStorage.setItem("currentMode", "light");
} else {
localStorage.setItem("currentMode", "dark");
}
}
},
false);
This solution assumes that:
Upvotes: 7
Reputation: 471
I believe the best way is to natively follow system settings unless user says otherwise.
Create button in your html. And then bind three-position switch to it with js. With saving to browser's LocalStorage.
And, finally, stylize your switch element.
document.addEventListener("DOMContentLoaded", function(event) {
switchTheme('.theme-switch');
});
function switchTheme(selector) {
const switches = document.querySelectorAll(selector);
// let colorTheme = localStorage.getItem('colorTheme') || 'system'; //commented to avoid security issue
let colorTheme = 'system';
function changeState() {
// localStorage.setItem('colorTheme', colorTheme); //commented to avoid security issue
document.documentElement.setAttribute('data-theme', colorTheme);
}
changeState();
switches.forEach(el => {
el.addEventListener('click', () => {
switch (colorTheme) {
case 'dark':
colorTheme = 'light';
break
case 'light':
colorTheme = 'system';
break
default:
colorTheme = 'dark';
}
changeState();
});
});
}
:root:not([data-theme="dark"]) {
--bg: #fff;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #000;
}
}
:root[data-theme="dark"] {
/* yep, you'll need to duplicate styles from above */
--bg: #000;
}
body {
background: var(--bg);
}
.theme-switch:after {
content: ': system';
}
:root[data-theme="dark"] .theme-switch:after {
content: ': dark';
}
:root[data-theme="light"] .theme-switch:after {
content: ': light';
}
<button class="theme-switch">Color scheme</button>
Upvotes: 1
Reputation: 95
My Solution (3 options in radio inputs: dark, system, light) adaptation of JimmyBanks and Meanderbilt Solution:
its a bit verbose I guess, but I struggled a bit to wrap my head around it
const themeSwitches = document.querySelectorAll('[data-color-theme-toggle]')
function removeColorThemeLocalStorage() {
localStorage.removeItem('color-theme')
}
function saveColorTheme(colorTheme) {
if (colorTheme === 'system') {
removeColorThemeLocalStorage()
return
}
localStorage.setItem('color-theme', colorTheme)
}
function applyColorTheme() {
const localStorageColorTheme = localStorage.getItem('color-theme')
const colorTheme = localStorageColorTheme || null
if (colorTheme) {
document.documentElement.setAttribute('data-color-theme', colorTheme)
}
}
function themeSwitchHandler() {
themeSwitches.forEach(themeSwitch => {
const el = themeSwitch
if (el.value === localStorage.getItem('color-theme')) {
el.checked = true
}
el.addEventListener('change', () => {
if (el.value !== 'system') {
saveColorTheme(el.value)
applyColorTheme(el.value)
} else {
removeColorThemeLocalStorage()
document.documentElement.removeAttribute('data-color-theme')
}
})
})
applyColorTheme()
}
document.addEventListener('DOMContentLoaded', () => {
themeSwitchHandler()
applyColorTheme()
})
html {
--hue-main: 220;
--color-text: hsl(var(--hue-main), 10%, 25%);
--color-text--high-contrast: hsl(var(--hue-main), 10%, 5%);
--color-link: hsl(var(--hue-main), 40%, 30%);
--color-background: hsl(var(--hue-main), 51%, 98.5%);
}
@media (prefers-color-scheme: dark) {
html.no-js {
--color-text: hsl(var(--hue-main), 5%, 60%);
--color-text--high-contrast: hsl(var(--hue-main), 10%, 80%);
--color-link: hsl(var(--hue-main), 60%, 60%);
--color-background: hsl(var(--hue-main), 10%, 12.5%);
}
}
[data-color-theme='dark'] {
--color-text: hsl(var(--hue-main), 5%, 60%);
--color-text--high-contrast: hsl(var(--hue-main), 10%, 80%);
--color-link: hsl(var(--hue-main), 60%, 60%);
--color-background: hsl(var(--hue-main), 10%, 12.5%);
}
<div class="color-scheme-toggle" role="group" title="select a color scheme">
<p>saved setting: <span class="theme-readout">...</span></p>
<input type="radio" name="scheme" id="dark" value="dark" aria-label="dark color scheme"> <label for="dark">dark</label>
<input type="radio" name="scheme" id="system" value="system" aria-label="system color scheme" checked="system"> <label for="system">system</label>
<input type="radio" name="scheme" id="light" value="light" aria-label="light color scheme"> <label for="light">light</label>
</div>
Upvotes: 0
Reputation: 2514
You can use my custom element <dark-mode-toggle>
that initially adheres to the user's prefers-color-scheme
setting, but that also allows the user to (permanently or temporarily) override it. The toggle works both with separate CSS files or with classes that are toggled. The README has examples for both approaches.
Upvotes: 13
Reputation: 4698
I have determined an appropriate solution, it is as follows:
CSS will use variables and themes:
// root/default variables
:root {
--font-color: #000;
--link-color:#1C75B9;
--link-white-color:#fff;
--bg-color: rgb(243,243,243);
}
//dark theme
[data-theme="dark"] {
--font-color: #c1bfbd;
--link-color:#0a86da;
--link-white-color:#c1bfbd;
--bg-color: #333;
}
The variables are then called where necessary, for example:
//the redundancy is for backwards compatibility with browsers that do not support CSS variables.
body
{
color:#000;
color:var(--font-color);
background:rgb(243,243,243);
background:var(--bg-color);
}
JavaScript is used to identify which theme the user has set, or if they have over-ridden their OS theme, as well as to toggle between the two, this is included in the header prior to the output of the html <body>...</body>
:
//determines if the user has a set theme
function detectColorScheme(){
var theme="light"; //default to light
//local storage is used to override OS theme settings
if(localStorage.getItem("theme")){
if(localStorage.getItem("theme") == "dark"){
var theme = "dark";
}
} else if(!window.matchMedia) {
//matchMedia method not supported
return false;
} else if(window.matchMedia("(prefers-color-scheme: dark)").matches) {
//OS theme setting detected as dark
var theme = "dark";
}
//dark theme preferred, set document with a `data-theme` attribute
if (theme=="dark") {
document.documentElement.setAttribute("data-theme", "dark");
}
}
detectColorScheme();
This javascript is used to toggle between the settings, it does not need to be included in the header of the page, but can be included wherever
//identify the toggle switch HTML element
const toggleSwitch = document.querySelector('#theme-switch input[type="checkbox"]');
//function that changes the theme, and sets a localStorage variable to track the theme between page loads
function switchTheme(e) {
if (e.target.checked) {
localStorage.setItem('theme', 'dark');
document.documentElement.setAttribute('data-theme', 'dark');
toggleSwitch.checked = true;
} else {
localStorage.setItem('theme', 'light');
document.documentElement.setAttribute('data-theme', 'light');
toggleSwitch.checked = false;
}
}
//listener for changing themes
toggleSwitch.addEventListener('change', switchTheme, false);
//pre-check the dark-theme checkbox if dark-theme is set
if (document.documentElement.getAttribute("data-theme") == "dark"){
toggleSwitch.checked = true;
}
finally, the HTML checkbox to toggle between themes:
<label id="theme-switch" class="theme-switch" for="checkbox_theme">
<input type="checkbox" id="checkbox_theme">
</label>
Through the use of CSS variables and JavaScript, we can automatically determine the users theme, apply it, and allow the user to over-ride it as well. [As of the current time of writing this (2019/06/10), only Firefox and Safari support the automatic theme detection]
Upvotes: 125