The Right Approach

The cleanest way to implement dark mode uses two things together: CSS custom properties (variables) for all your colours, and the prefers-color-scheme media query to detect the user's OS preference.

This means zero JavaScript is required for basic dark mode. The toggle button (if you want one) is the only part that needs JS.

Set Up CSS Variables

Define all colours as CSS variables on :root for light mode, then override them inside the dark mode media query:

:root {
  --bg: #ffffff;
  --surface: #f8fafc;
  --text: #0f172a;
  --muted: #64748b;
  --border: #e2e8f0;
  --accent: #6366f1;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #060b14;
    --surface: #0b1220;
    --text: #dde8f8;
    --muted: #4a6080;
    --border: #0f1e32;
  }
}

The Media Query

Now use only your CSS variables throughout your stylesheet — never hardcode colour values:

body {
  background: var(--bg);       /* ✅ correct */
  color: var(--text);
}

/* NEVER do this: */
body {
  background: #ffffff;         /* ❌ won't respond to dark mode */
}
💡 Tip: If you're starting a new project, establish this variable system from the very beginning. Retrofitting an existing site means finding every hardcoded colour — time-consuming but worth it.

JavaScript Toggle Button

To let users manually override their OS preference, add a toggle. Use a data-theme attribute on <html>:

/* Override via data-theme attribute */
[data-theme="dark"] {
  --bg: #060b14;
  --surface: #0b1220;
  --text: #dde8f8;
  --muted: #4a6080;
  --border: #0f1e32;
}

[data-theme="light"] {
  --bg: #ffffff;
  --surface: #f8fafc;
  --text: #0f172a;
  --muted: #64748b;
  --border: #e2e8f0;
}
const toggle = document.getElementById('theme-toggle');
toggle.addEventListener('click', () => {
  const current = document.documentElement.getAttribute('data-theme');
  const next = current === 'dark' ? 'light' : 'dark';
  document.documentElement.setAttribute('data-theme', next);
  localStorage.setItem('theme', next);
});

Saving the Preference

Apply the saved preference before the page renders to prevent a flash of the wrong theme:

/* In <head> — before any CSS loads */
<script>
  const saved = localStorage.getItem('theme');
  const preferred = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  document.documentElement.setAttribute('data-theme', saved || preferred);
</script>
⚠️ Important: This script must be in <head> before your CSS link — not at the bottom of <body>. If it runs after render, users will see a white flash on dark-mode pages.