Frontend Development 11 min read

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.

Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Mastering Dark Mode: Hand‑Crafted CSS & JavaScript Techniques

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-theme

class on the

body

element.

<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>&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;link href="light-theme.css" rel="stylesheet" id="theme-link"&gt;
  &lt;/head&gt;
&lt;/html&gt;</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-scheme

media 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>
filter example
filter example

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

img

tags or use pseudo‑elements, though both have drawbacks.

background image issue
background image issue

Filter Affects Fixed Elements

Applying

filter

to a non‑root element creates a new containing block, breaking

position: fixed

and

position: absolute

elements. Apply the filter to the

html

element or wrap fixed components in a separate container.

fixed element broken
fixed element broken

Server‑Rendered Pages Missing Dark‑Mode Class

When a page is rendered on the server,

window.matchMedia

is unavailable, so the initial HTML may lack the

dark-mode

class. After hydration, the client‑side script must add the class based on the stored preference; otherwise components render with the wrong theme.

frontendJavaScriptWeb DevelopmentCSSfilterdark mode
Tencent IMWeb Frontend Team
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.