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.

const btn = document.querySelector('.btn-toggle');

btn.addEventListener('click', function() {
  document.body.classList.toggle('dark-theme');
});

Two CSS approaches are shown.

/* Method 1: Separate styles */
body {
  color: #222;
  background: #fff;
  a { color: #0033cc; }
}
body.dark-theme {
  color: #eee;
  background: #121212;
  a { color: #809fff; }
}
/* 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); }
}

Another option is to swap entire stylesheet files.

<html lang="en">
  <head>
    <link href="light-theme.css" rel="stylesheet" id="theme-link">
  </head>
</html>
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';
  }
});

Follow System

Use the prefers-color-scheme media query or JavaScript detection to match the OS theme.

/* Direct media query */
@media (prefers-color-scheme: dark) { }
@media (prefers-color-scheme: light) { }
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
  document.body.classList.add('dark-theme');
} else {
  document.body.classList.remove('dark-theme');
}

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.

html {
  filter: invert(1) hue-rotate(180deg);
}

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.

html {
  filter: invert(1) hue-rotate(180deg);
  img {
    filter: invert(1) hue-rotate(180deg);
  }
}
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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

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

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.