Programming Beginner 9 min

How to Add a Dark Mode Toggle to Your Website

Dark mode is no longer optional — users expect it. The correct implementation uses CSS custom properties for every color, a data-theme attribute on <html> to switch themes, and a small inline script in <head> to apply the saved preference before the first paint. Done right, there is zero flash of the wrong theme and zero JavaScript dependency for the styling itself.

Step-by-step

  1. 1

    Define color tokens in :root

    Replace every hardcoded color in your CSS with a custom property. Define all light-mode values on :root. This is your single source of truth — no color should appear anywhere else as a literal hex or rgb value.

    css
    :root {
      --bg:        #ffffff;
      --bg-subtle: #f5f5f5;
      --surface:   #ffffff;
      --border:    #e0e0e0;
      --text:      #111111;
      --text-muted:#666666;
      --accent:    #0070f3;
      --accent-hover: #0050c9;
    }
  2. 2

    Override tokens under [data-theme="dark"]

    Add a second block that re-declares only the color tokens with their dark-mode values. Everything else in your CSS stays unchanged — components just pick up the new variable values automatically.

    css
    [data-theme="dark"] {
      --bg:        #0d0d0d;
      --bg-subtle: #1a1a1a;
      --surface:   #1e1e1e;
      --border:    #2e2e2e;
      --text:      #f0f0f0;
      --text-muted:#a0a0a0;
      --accent:    #3b9eff;
      --accent-hover: #60b0ff;
    }
  3. 3

    Apply tokens to all elements

    Go through your stylesheet and replace hardcoded colors. Use var() everywhere — backgrounds, text, borders, shadows, inputs, buttons. If you miss a color it will not respond to the theme switch.

    css
    body {
      background-color: var(--bg);
      color: var(--text);
    }
    
    .card {
      background: var(--surface);
      border: 1px solid var(--border);
    }
    
    a {
      color: var(--accent);
    }
    a:hover {
      color: var(--accent-hover);
    }
  4. 4

    Add the toggle button

    Put a button in your nav or header. The icon will update via JavaScript. Keep it simple — one button, one job.

    html
    <button id="theme-toggle" aria-label="Toggle dark mode">
      <span id="theme-icon">🌙</span>
    </button>
  5. 5

    Write the toggle JavaScript

    The script reads the current theme from the data-theme attribute, flips it, saves it to localStorage, and updates the button icon. Place this in your main JS file or a <script> at the end of <body>.

    javascript
    const btn  = document.getElementById('theme-toggle');
    const icon = document.getElementById('theme-icon');
    const root = document.documentElement;
    
    btn.addEventListener('click', () => {
      const isDark = root.dataset.theme === 'dark';
      const next   = isDark ? 'light' : 'dark';
    
      root.dataset.theme = next;
      localStorage.setItem('theme', next);
      icon.textContent   = next === 'dark' ? '☀️' : '🌙';
    });
  6. 6

    Read prefers-color-scheme as the default

    For first-time visitors who have no saved preference, read the OS-level setting via prefers-color-scheme: dark. This is the only respectful default — do not force light mode on someone running dark mode everywhere.

    javascript
    // Called once on load to set the initial icon
    function applyStoredTheme() {
      const saved  = localStorage.getItem('theme');
      const system = window.matchMedia('(prefers-color-scheme: dark)').matches
                       ? 'dark' : 'light';
      const theme  = saved ?? system;
    
      document.documentElement.dataset.theme = theme;
      document.getElementById('theme-icon').textContent =
        theme === 'dark' ? '☀️' : '🌙';
    }
    
    applyStoredTheme();
  7. 7

    Prevent the FOUC with an inline head script

    If the theme JS runs after the page renders, visitors on dark mode will briefly see the light theme flash in. The fix: a tiny inline <script> in <head> — before any CSS link — that sets data-theme synchronously. It runs before paint, so there is nothing to flash.

    This script must be inline, not a deferred or async external file.

    html
    <!-- Place this as the FIRST element inside <head> -->
    <script>
      (function () {
        var saved  = localStorage.getItem("theme");
        var system = window.matchMedia("(prefers-color-scheme: dark)").matches
                       ? "dark" : "light";
        document.documentElement.dataset.theme = saved || system;
      })();
    </script>
  8. 8

    Smooth the transition

    Add a short transition on properties that change between themes so the switch does not feel jarring. Only transition the color-related properties — not layout properties like width or transform, which would slow every animation.

    css
    *, *::before, *::after {
      transition:
        background-color 0.2s ease,
        color            0.2s ease,
        border-color     0.2s ease;
    }

Tips & gotchas

  • Never use `prefers-color-scheme` in a `@media` block for your main theme colors — it cannot be overridden by a user toggle. Use `data-theme` instead.
  • Store the user's preference as "light" or "dark", not as a boolean — it is easier to extend to a third theme later.
  • Test with JavaScript disabled: the inline head script still runs, so the theme is applied, but the toggle button will not work. That is acceptable.
  • If you use CSS-in-JS or Tailwind, the same principle applies — define the token names once and swap the values via a class or attribute on `<html>`.
  • Icons and images may need separate dark-mode versions. Use `picture` with a `media="(prefers-color-scheme: dark)"` source or swap `src` via JS alongside the theme change.

Wrapping up

A proper dark mode implementation is built on three pillars: CSS custom properties that make every color swappable, a data-theme attribute that does the actual switching, and an inline head script that eliminates the theme flash. Get these three right and the rest — toggle button, persistence, OS default — follows naturally. The entire implementation is under 50 lines of code.

#CSS #JavaScript #UX
Back to all guides

Need Help With Your Project?

Book a free 30-minute consultation to discuss your technical challenges and explore solutions together.