Skip to content

Dark Theme

samuelgfeller edited this page Apr 4, 2024 · 6 revisions

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.

Click here to see the 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.

Click here to see the 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);
}
Clone this wiki locally