Dark Theme
Table of contents
Loading...Introduction
A dark mode is not hard to implement and is appreciated by many users. Some people prefer the look of a dark theme, and it can improve eye comfort by reducing strain on the eyes.
The switch
In this example, a switch is used to enable or disable the dark mode, but it could be a dropdown menu or a button.
Design 1
From the Slim Example Project.
Show HTML and CSS code for the switch
HTML
<label id="dark-mode-switch-container">
<input id="dark-mode-toggle-checkbox" type="checkbox">
<div id="dark-mode-toggle-slot">
<div id="dark-mode-sun-icon-wrapper">
<img src="assets/general/dark-mode/sun-icon.svg" alt="sun" id="dark-mode-sun-icon">
</div>
<div id="dark-mode-toggle-button"></div>
<div id="dark-mode-moon-icon-wrapper">
<img src="assets/general/dark-mode/moon-icon.svg" alt="sun" id="dark-mode-moon-icon">
</div>
</div>
</label>
CSS
File: dark-mode-toggle-switch.css
:root {
--dark-mode-toggle-size: 0.3;
}
#dark-mode-switch-container{
margin: 15px 0 0 0;
display: inline-block;
cursor: pointer;
}
#dark-mode-toggle-checkbox {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
#dark-mode-toggle-slot {
position: relative;
height: calc(10.3em * var(--dark-mode-toggle-size));
width: calc(20em * var(--dark-mode-toggle-size));
border: calc(5px * var(--dark-mode-toggle-size)) solid #e4e7ec;
border-radius: calc(10em * var(--dark-mode-toggle-size));
background-color: white;
/*box-shadow: 0px 2px 5px white;*/
transition: background-color 250ms, border-color 250ms;
}
#dark-mode-toggle-checkbox:checked ~ #dark-mode-toggle-slot {
border-color: var(--background-accent-color);
background-color: var(--background-accent-color);
}
#dark-mode-toggle-button {
transform: translate(calc(11.75em * var(--dark-mode-toggle-size)), calc(1.75em * var(--dark-mode-toggle-size)));
position: absolute;
height: calc(6.5em * var(--dark-mode-toggle-size));
width: calc(6.5em * var(--dark-mode-toggle-size));
border-radius: 50%;
background-color: #ffeccf;
box-shadow: inset 0px 0px 0px calc(0.75em * var(--dark-mode-toggle-size)) #ffbb52;
transition: background-color 250ms, border-color 250ms, transform 500ms cubic-bezier(.26,2,.46,.71);
}
#dark-mode-toggle-checkbox:checked ~ #dark-mode-toggle-slot #dark-mode-toggle-button {
background-color: #3f495b;
box-shadow: inset 0px 0px 0px 0.15em var(--primary-text-color);
transform: translate(calc(1.75em * var(--dark-mode-toggle-size)), calc(1.75em * var(--dark-mode-toggle-size)));
}
#dark-mode-sun-icon {
position: absolute;
height: calc(6em * var(--dark-mode-toggle-size));
width: calc(6em * var(--dark-mode-toggle-size));
filter: invert(83%) sepia(100%) saturate(1000%) hue-rotate(310deg) brightness(95%) contrast(92%);;
/*color: #ffbb52;*/
}
#dark-mode-sun-icon-wrapper {
position: absolute;
height: calc(6em * var(--dark-mode-toggle-size));
width: calc(6em * var(--dark-mode-toggle-size));
opacity: 1;
transform: translate(calc(2em * var(--dark-mode-toggle-size)), calc(2em * var(--dark-mode-toggle-size))) rotate(15deg);
transform-origin: 50% 50%;
transition: opacity 150ms, transform 500ms cubic-bezier(.26,2,.46,.71);
}
#dark-mode-toggle-checkbox:checked ~ #dark-mode-toggle-slot #dark-mode-sun-icon-wrapper {
opacity: 0;
transform: translate(calc(3em * var(--dark-mode-toggle-size)), calc(2em * var(--dark-mode-toggle-size))) rotate(0deg);
}
#dark-mode-moon-icon {
position: absolute;
height: calc(6em * var(--dark-mode-toggle-size));
width: calc(6em * var(--dark-mode-toggle-size));
filter: invert(86%) sepia(7%) saturate(254%) hue-rotate(163deg) brightness(94%) contrast(90%);;
}
#dark-mode-moon-icon-wrapper {
position: absolute;
height: calc(6em * var(--dark-mode-toggle-size));
width: calc(6em * var(--dark-mode-toggle-size));
opacity: 0;
transform: translate(calc(11em * var(--dark-mode-toggle-size)), calc(2em * var(--dark-mode-toggle-size))) rotate(0deg);
transform-origin: 50% 50%;
transition: opacity 150ms, transform 500ms cubic-bezier(.26,2.5,.46,.71);
}
#dark-mode-toggle-checkbox:checked ~ #dark-mode-toggle-slot #dark-mode-moon-icon-wrapper {
opacity: 1;
transform: translate(calc(12em * var(--dark-mode-toggle-size)), calc(2em * var(--dark-mode-toggle-size))) rotate(-15deg);
}
Design 2
From the Slim Starter project.
Show HTML, CSS and JS code for the button
HTML
<div id="dark-theme-toggle"></div>
CSS
File: dark-mode-toggle-button.css
#dark-theme-toggle {
position: absolute;
width: 35px;
height: 35px;
border-radius: 50%;
background: linear-gradient(180deg, black 50%, #d0d0d0 50%);
cursor: pointer;
transition: background 0.3s ease-in-out;
}
#dark-theme-toggle.dark-theme-enabled {
background: linear-gradient(180deg, #d0d0d0 50%, black 50%);
}
JavaScript
The JS script is slightly different for the button than for the switch.
Later, the code for the switch will be shown.
This is the code for the button:
// Get the toggle element
const toggleButton = document.querySelector('#dark-theme-toggle');
if (toggleButton) {
// Add event listener to the toggle switch for theme switching
toggleButton.addEventListener('click', switchTheme, false);
// Retrieve the current theme from localStorage
const currentTheme = localStorage.getItem('theme') ? localStorage.getItem('theme') : null;
// Set the theme based on the stored value from localStorage
if (currentTheme) {
// Set the data-theme attribute on the html element
document.documentElement.setAttribute('data-theme', currentTheme);
// Check the toggle switch if the current theme is 'dark'
if (currentTheme === 'dark') {
toggleButton.classList.add('dark-theme-enabled');
}
}
}
/**
* Handle theme switching with localstorage
*
* @param e
*/
function switchTheme(e) {
let theme;
// Check the current theme and switch to the opposite theme
if (document.documentElement.getAttribute('data-theme') === 'dark') {
theme = 'light';
toggleButton.classList.remove('dark-theme-enabled');
} else {
theme = 'dark';
toggleButton.classList.add('dark-theme-enabled');
}
// Set html data-attribute and local storage entry
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
// Make ajax call to change value in database
// let userId = document.getElementById('user-id').value;
// submitUpdate({theme: theme}, `users/${userId}`)
// .then(r => {
// }).catch(errorMsg => {
// displayFlashMessage('error', 'Failed to change the theme in the database.')
// });
}
Implementation
An easy way to implement the dark mode is to set a theme
data-attribute
on a parent element of the page such as <html>
or <body>
.
CSS will use that attribute
to decide what value the color variables hold.
Switching the theme
When the switch is toggled, the theme
data-attribute is set to dark
or light
.
The chosen theme should also be stored in the browser's local storage and ideally in the database if users can log in.
This is what the script below does:
- Retrieve the current theme from the browser's local storage.
- If the toggle switch element exists, it adds an event listener to it that triggers
the
switchTheme
function. - If a theme is stored in local storage, it sets the data-theme attribute on the
<html>
element. - The
switchTheme
function checks the current theme and switches to the opposite theme. It also makes an Ajax call to update the theme in the database for the current user.
File: public/assets/general/dark-mode/dark-mode.js
import {submitUpdate} from "../ajax/submit-update-data.js";
import {displayFlashMessage} from "../page-component/flash-message/flash-message.js";
// Get the toggle switch element
const toggleSwitch = document.querySelector('#dark-mode-toggle-checkbox');
if (toggleSwitch) {
// Add event listener to the toggle switch for theme switching
toggleSwitch.addEventListener('change', switchTheme, false);
// Retrieve the current theme from localStorage
const currentTheme = localStorage.getItem('theme') ? localStorage.getItem('theme') : null;
// Set the theme based on the stored value from localStorage
if (currentTheme) {
// Set the data-theme attribute on the html element
document.documentElement.setAttribute('data-theme', currentTheme);
// Check the toggle switch if the current theme is 'dark'
if (currentTheme === 'dark') {
toggleSwitch.checked = true;
}
}
}
function switchTheme(e) {
let theme;
// Check the current theme and switch to the opposite theme
if (document.documentElement.getAttribute('data-theme') === 'dark') {
theme = 'light';
} else {
theme = 'dark';
}
// Set html data-attribute and local storage entry
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
// Make ajax call to change value in database
let userId = document.getElementById('user-id').value;
submitUpdate({theme: theme}, `users/${userId}`, true)
.then(r => {
}).catch(r => {
displayFlashMessage('error', 'Failed to change the theme in the database.')
});
}
Setting the theme on page load
On each page load, the theme data-attribute must be set to the current theme preferably as early as possible to avoid a flash of the wrong theme while the scripts are loading.
An inline script in the layout template is a good place to do this because it is loaded on every page and inline scripts are executed immediately.
To account for the theme stored in the database (if there is one), the theme is added to the URL
as a GET parameter by the server when the user logs in.
The following script is also in charge of retrieving and storing this value in the browser's
local storage.
<!DOCTYPE html>
<html>
<head>
<!-- ... -->
<script>
// Add the theme immediately to the <html> element before everything else for the correct colors
const theme = localStorage.getItem('theme') ? localStorage.getItem('theme') : null;
// Get the theme provided from the server via query param (available only after login)
const themeParam = new URLSearchParams(window.location.search).get('theme');
// Finally, add the theme to the <html> element
document.documentElement.setAttribute('data-theme', themeParam ?? theme ?? 'light');
// If a theme from the database is provided and is different from localStorage, replace localStorage value
if (themeParam && themeParam !== theme) {
localStorage.setItem('theme', themeParam);
}
</script>
</head>
<body>
<!-- ... -->
</body>
</html>
Defining the colors
The colors of the page elements are defined in CSS variables in the stylesheets.
Websites usually use the same colors for a lot of elements, to have a consistent look. The collection of these colors can be defined in a CSS file loaded by all pages.
Colors for the default theme are stored under the pseudo-class
:root
.
The colors for the dark theme are defined with the data-attribute selector
[data-theme="dark"]
which overrides the default colors when the <html>
element
has the data-theme="dark"
attribute.
/* Light theme color */
:root {
--primary-color: #2e3e50;
/* Background colors */
--background-color: white;
--background-accent-color: #efefef;
--background-accent-1-color: #eaeaea;
/* Text */
--primary-text-color: #2e3e50;
--secondary-text-color: rgba(46, 62, 80, 0.80);
--title-color: black;
/* Filters */
/* Styles the black svg icons to the primary color #2e3e50 */
--primary-color-filter: invert(20%) sepia(9%) saturate(2106%) hue-rotate(172deg) brightness(93%) contrast(86%);
}
/* Dark theme colors */
[data-theme="dark"] {
--primary-color: #4f6b8a;
/* Background colors */
--background-color: #101213;
--background-accent-color: #1f2425;
--background-accent-1-color: #262b31;
/* Text */
--primary-text-color: #c3cad0;
--secondary-text-color: #919fac;
--title-color: #c3cad0;
/* Filters */
/* Styles the black svg icons to a color similar to the primary color */
--primary-color-filter: invert(45%) sepia(10%) saturate(1191%) hue-rotate(171deg) brightness(100%) contrast(100%);
}
SVG icon color
The color of svg icons can be changed by the CSS filter
property.
They are black by default, and the --primary-color-filter
changes the color to the
primary color of the theme.
Tools like angel-rs.github.io/css-color-filter-generator
or codepen.io/sosuke/pen/Pjoqqp can be used to generate the filter.
Using the colors
The color variables can be used in the CSS like this:
body{
background-color: var(--background-color);
color: var(--primary-text-color);
}
h1, h2, h3{
color: var(--title-color);
}
.icon {
filter: var(--primary-color-filter);
border: 1px solid var(--primary-color);
}