Class-Based Dark Mode Toggle with CSS Variables

Implement a user-controlled dark mode toggle using a .dark class on the HTML element with CSS custom property overrides and localStorage persistence.

Dark Mode

Detailed Explanation

Class-Based Dark Mode Toggle

While prefers-color-scheme is automatic, many apps want to give users explicit control. The class-based approach adds a .dark class to <html>, overriding CSS variables.

CSS Structure

:root {
  --bg: #ffffff;
  --surface: #f4f4f5;
  --text: #18181b;
  --text-muted: #71717a;
  --primary: #2563eb;
  --border: #e4e4e7;
}

html.dark {
  --bg: #0a0a0b;
  --surface: #141415;
  --text: #fafafa;
  --text-muted: #a1a1aa;
  --primary: #3b82f6;
  --border: #27272a;
}

JavaScript Toggle

const toggle = document.getElementById('theme-toggle');
toggle.addEventListener('click', () => {
  document.documentElement.classList.toggle('dark');
  const isDark = document.documentElement.classList.contains('dark');
  localStorage.setItem('theme', isDark ? 'dark' : 'light');
});

Preventing Flash of Wrong Theme

Place a blocking script in <head> before any stylesheets or body content:

<script>
  if (localStorage.getItem('theme') === 'dark' ||
      (!localStorage.getItem('theme') &&
       window.matchMedia('(prefers-color-scheme: dark)').matches)) {
    document.documentElement.classList.add('dark');
  }
</script>

Framework Integration

  • Next.js: Use next-themes which handles SSR, hydration, and flash prevention.
  • Astro: Use is:inline scripts for the blocking check.
  • Plain HTML: The <head> script above works without any framework.

Accessibility

Include aria-label on your toggle button and announce the current state. Users relying on screen readers need to know whether the toggle switches to light or dark mode.

Use Case

Applications that need a user-facing dark mode toggle button with localStorage persistence, working alongside OS-level preference detection as a fallback.

Try It — CSS Variable Generator

Open full tool