-
-
Notifications
You must be signed in to change notification settings - Fork 4
Dark Theme
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.
In this example, a switch is used to enable or disable the dark mode, but it could be a dropdown menu or a button.
From the Slim Example Project.
Click here to see the HTML and CSS code for the switch
<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>
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);
}
From the Slim Starter project.
Click here to see the HTML, CSS and JS code for the button
<div id="dark-theme-toggle"></div>
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%);
}
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.')
// });
}
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.
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.')
});
}
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>
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%);
}
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.
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);
}
Slim app basics
- Composer
- Web Server config and Bootstrapping
- Dependency Injection
- Configuration
- Routing
- Middleware
- Architecture
- Single Responsibility Principle
- Action
- Domain
- Repository and Query Builder
Features
- Logging
- Validation
- Session and Flash
- Authentication
- Authorization
- Translations
- Mailing
- Console commands
- Database migrations
- Error handling
- Security
- API endpoint
- GitHub Actions
- Scrutinizer
- Coding standards fixer
- PHPStan static code analysis
Testing
Frontend
Other