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-themeswhich handles SSR, hydration, and flash prevention. - Astro: Use
is:inlinescripts 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.