Mastering Dark Mode: Hand‑Crafted CSS & JavaScript Techniques
This article walks through practical ways to implement true dark mode on web pages—including manual toggles, system‑preference detection, CSS variables, stylesheet swapping, filter tricks, and common pitfalls—while providing complete code examples and explanations.
Implementing a genuine dark mode often requires hand‑written code rather than a simple overlay. Mobile QQ recently demanded proper dark‑mode support, prompting a full rewrite of the theme logic.
Common Practices
Before coding, the author recommends reading CSS‑Tricks’ A Complete Guide to Dark Mode on the Web for a solid overview.
Activation Methods
There are two main ways to activate dark mode: a manual switch that toggles a class on the page, or automatic detection based on the system or application preference.
Manual Toggle
Attach an event handler to a toggle button that adds or removes a
dark-themeclass on the
bodyelement.
<code>const btn = document.querySelector('.btn-toggle');
btn.addEventListener('click', function() {
document.body.classList.toggle('dark-theme');
});</code>Two CSS approaches are shown.
<code>/* Method 1: Separate styles */
body {
color: #222;
background: #fff;
a { color: #0033cc; }
}
body.dark-theme {
color: #eee;
background: #121212;
a { color: #809fff; }
}</code> <code>/* Method 2: CSS variables */
body {
--text-color: #222;
--bkg-color: #fff;
--anchor-color: #0033cc;
}
body.dark-theme {
--text-color: #eee;
--bkg-color: #121212;
--anchor-color: #809fff;
}
body {
color: var(--text-color);
background: var(--bkg-color);
a { color: var(--anchor-color); }
}</code>Another option is to swap entire stylesheet files.
<code><html lang="en">
<head>
<link href="light-theme.css" rel="stylesheet" id="theme-link">
</head>
</html></code> <code>const btn = document.querySelector('.btn-toggle');
const theme = document.querySelector('#theme-link');
btn.addEventListener('click', function() {
if (theme.getAttribute('href') == 'light-theme.css') {
theme.href = 'dark-theme.css';
} else {
theme.href = 'light-theme.css';
}
});</code>Follow System
Use the
prefers-color-schememedia query or JavaScript detection to match the OS theme.
<code>/* Direct media query */
@media (prefers-color-scheme: dark) { }
@media (prefers-color-scheme: light) { }</code> <code>if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark-theme');
} else {
document.body.classList.remove('dark-theme');
}</code>User Preference
Persist a user’s choice in
localStorage(or cookies for server‑rendered pages) so the site remembers the selected theme across visits.
Tricks
A one‑line CSS filter can produce a quick dark mode, though it has limitations.
<code>html {
filter: invert(1) hue-rotate(180deg);
}</code>Explanation:
invert()flips color channels, while
hue-rotate()rotates the hue wheel. The article shows numeric examples of how colors change after inversion.
Images also get inverted, so they must be re‑inverted to appear normal.
<code>html {
filter: invert(1) hue-rotate(180deg);
img {
filter: invert(1) hue-rotate(180deg);
}
}</code>Dark Mode Pitfalls
Reverse‑Engineering Target Colors
When using
filter, start from a light base color and let the filter produce the desired dark shade. Determining the original light color from a design’s dark color can be done by applying the filter to a test element and reading the resulting color.
Background Image Inversion Issue
Filters do not affect elements styled with
background‑image, and such elements cannot be selected via
img. The workaround is to replace background images with
imgtags or use pseudo‑elements, though both have drawbacks.
Filter Affects Fixed Elements
Applying
filterto a non‑root element creates a new containing block, breaking
position: fixedand
position: absoluteelements. Apply the filter to the
htmlelement or wrap fixed components in a separate container.
Server‑Rendered Pages Missing Dark‑Mode Class
When a page is rendered on the server,
window.matchMediais unavailable, so the initial HTML may lack the
dark-modeclass. After hydration, the client‑side script must add the class based on the stored preference; otherwise components render with the wrong theme.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.