Using vanilla HTML, CSS, JavaScript, on a static site.
Credit: Ryan Feigenbaum.
Developers love dark mode, especially during the night. Supporting dark mode on the page is not only beneficial, but also arguably imperative, given that human eyes don't handle sudden change in brightness well.
- Have a predefined mapping color values between light and dark modes.
- User can toggle between light and dark modes.
- The initial color mode on page load respects user's setting, and if not found, the system preference.
In modern browsers, there are several features to take advantage of:
- HTML custom attributes
data-*
- CSS custom properties
--*
- media query
prefers-color-scheme
in JavaScript Web APIwindow.matchMedia
and in CSS@media
.
In the <html>
tag, add custom attribute data-color-mode
and its default
value light
.
<html data-color-mode="light">
<head>
<title>Play with color modes</title>
... css, javascript ...
</head>
<body>
<div>This is a test.</div>
</body>
</html>
Use CSS custom properties to define color schemes for both the light and dark modes.
Name the colors in a way that is agnostic to the color mode, e.g. avoid adjectives like "light" or "dark", because while a dark shade of color appears salient in light mode, it would be inconspicuous in dark mode.
Use the :root
pseudo-element so that the custom properties can be applied to
any element. Use the attribute selector [data-color-mode="..."]
to specify
the color mode.
:root[data-color-mode="light"] {
--bg-color: #fff;
--fg-color: #000;
--red: #f9e9e7;
--red-bold: #c84031;
--green: #e9f4eb;
--green-bold: #3c7d40;
--yellow: #faf4df;
--yellow-bold: #eeac3c;
}
:root[data-color-mode="dark"] {
--bg-color: #202124;
--fg-color: #e8eaed;
--red: #554141;
--red-bold: #e49086;
--green: #414f46;
--green-bold: #91c699;
--yellow: #59533d;
--yellow-bold: #f7d575;
}
You can apply the color schemes in your CSS definitions, like this:
body {
background-color: var(--bg-color);
color: var(--fg-color);
border-color: var(--green);
border-style: solid;
border-width: 1px;
font-family: 'Open Sans', 'Helvetica', sans-serif;
font-size: 16px;
margin: 0;
}
Implement UI widgets to toggle between the two color modes. For a prettier appearance, I used SVG to render a sun and a moon. Arrange HTML and CSS so that only one toggle is shown on page: in light mode, the moon; in dark mode, the sun.
HTML:
<!-- Define the SVG -->
<svg style="display: none;">
<symbol viewBox="0 0 24 24" id="color_mode_control_icon_sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</symbol>
<symbol viewBox="0 0 24 24" id="color_mode_control_icon_moon">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</symbol>
</svg>
<!-- The toggles -->
<button class="color_mode_button hidden_in_light_mode" id="enable_light_mode">
<svg>
<title>Light mode</title>
<use href="#color_mode_control_icon_sun"></use>
</svg>
</button>
<button class="color_mode_button hidden_in_dark_mode" id="enable_dark_mode">
<svg>
<title>Dark mode</title>
<use href="#color_mode_control_icon_moon"></use>
</svg>
</button>
CSS:
:root[data-color-mode="light"] .hidden_in_light_mode {
display: none;
}
:root[data-color-mode="dark"] .hidden_in_dark_mode {
display: none;
}
.color_mode_button {
display: flex;
align-items: right;
justify-content: right;
padding: 5px;
margin: 0 auto 1.5rem;
font-size: 1rem;
font-weight: 600;
line-height: 1;
color: var(--fg-color);
cursor: pointer;
background: none;
border: none;
border-radius: 50px;
}
.color_mode_button svg {
width: 1.5em;
height: 1.5em;
fill: none;
stroke: var(--fg-color);
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1.5px;
}
.color_mode_button#enable_dark_mode:hover svg,
.color_mode_button#enable_dark_mode:focus svg {
outline: none;
fill: var(--blue-bold);
}
.color_mode_button#enable_light_mode:hover svg,
.color_mode_button#enable_light_mode:focus svg {
outline: none;
fill: var(--yellow-bold);
}
JavaScript:
document.querySelectorAll('.color_mode_button').forEach((button) => {
button.addEventListener('click', (event) => {
const targetElement = /** @type {HTMLElement} */ (event.currentTarget);
const elementId = targetElement.id;
if (elementId === 'enable_light_mode') {
document.documentElement.setAttribute('data-color-mode', 'light');
// localStorage.setItem('data-color-mode', 'light');
} else if (elementId === 'enable_dark_mode') {
document.documentElement.setAttribute('data-color-mode', 'dark');
// localStorage.setItem('data-color-mode', 'dark');
} else {
throw new Error(`unrecognized color mode button id ${elementId}.`)
}
});
});
You are almost there! Check out the effects in your browser.
We want the browser to remember what color mode the user has set, so after a
reload the page can stay in that color mode. To do that, we use the browser's
localStorage
.
Uncomment the lines containing localStorage.setItem(...)
in the JavaScript
snippet above, and add JavaScript:
// Assuming the default color mode is 'light' in Step 1.
if (localStorage.getItem('data-color-mode') === 'dark') {
document.documentElement.setAttribute('data-color-mode', 'dark');
}
We also want the color mode to have a proper initial value that takes the
system preference into account. To do that, we use media query
prefers-color-scheme
using the Web API
matchMedia
.
Replace the JavaScript added in the previous step with this:
// Assuming the default color mode is 'light' in Step 1.
const matcher = window.matchMedia('(prefers-color-scheme: dark)');
if (localStorage.getItem('data-color-mode') === 'dark' ||
(matcher.matches && !localStorage.getItem('data-color-mode'))) {
document.documentElement.setAttribute('data-color-mode', 'dark');
}
Done.
We want the icon shown in the browser tab be aware of the system preference of color mode as well. To do that, you need to have two versions of icon images ready, and put embed this JavaScript snippet on the page:
const matcher = window.matchMedia('(prefers-color-scheme: dark)');
function onUpdate() {
const prevIcon = document.querySelector('link#favicon');
if (prevIcon) {
prevIcon.remove();
}
const icon = document.createElement('link');
icon.rel = 'icon';
icon.id = 'favicon';
icon.href = matcher.matches ? 'favicon_for_dark_mode.png' :
'favicon_for_light_mode.png';
document.head.append(icon);
}
matcher.addListener(onUpdate);
onUpdate();
■