DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Implementing Dark Mode: CSS Variables, System Preference, and Persistence

Implementing Dark Mode: CSS Variables, System Preference, and Persistence

Dark mode done right: system preference detection, manual toggle, and persistence — without a flash of white on load.

CSS Variables: The Foundation

:root {
  --bg-primary: #ffffff;
  --bg-secondary: #f8f9fa;
  --text-primary: #1a1a1a;
  --text-secondary: #6b7280;
  --border: #e5e7eb;
  --shadow: rgba(0, 0, 0, 0.1);
}

[data-theme='dark'] {
  --bg-primary: #0f172a;
  --bg-secondary: #1e293b;
  --text-primary: #f1f5f9;
  --text-secondary: #94a3b8;
  --border: #334155;
  --shadow: rgba(0, 0, 0, 0.4);
}

/* Components use variables — not hardcoded colors */
body { background: var(--bg-primary); color: var(--text-primary); }
.card { background: var(--bg-secondary); border: 1px solid var(--border); }
Enter fullscreen mode Exit fullscreen mode

System Preference Detection

/* Apply dark mode when system prefers it */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --bg-primary: #0f172a;
    /* ... */
  }
}
Enter fullscreen mode Exit fullscreen mode

React Hook

function useTheme() {
  const [theme, setTheme] = useState<'light' | 'dark' | 'system'>(() => {
    if (typeof window === 'undefined') return 'system';
    return (localStorage.getItem('theme') as 'light' | 'dark' | 'system') ?? 'system';
  });

  useEffect(() => {
    const root = document.documentElement;

    if (theme === 'system') {
      root.removeAttribute('data-theme');
    } else {
      root.setAttribute('data-theme', theme);
    }

    localStorage.setItem('theme', theme);
  }, [theme]);

  return { theme, setTheme };
}
Enter fullscreen mode Exit fullscreen mode

Preventing Flash of Wrong Theme

<!-- In <head> — runs before body renders -->
<script>
  (function() {
    const stored = localStorage.getItem('theme');
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    const theme = stored === 'dark' || (!stored && prefersDark) ? 'dark' : 'light';
    document.documentElement.setAttribute('data-theme', theme);
  })();
</script>
Enter fullscreen mode Exit fullscreen mode

Tailwind Dark Mode

// tailwind.config.js
module.exports = {
  darkMode: 'class', // or 'media' for system-only
};
Enter fullscreen mode Exit fullscreen mode
// Toggle dark class on <html>
function ThemeToggle() {
  return (
    <button
      onClick={() => document.documentElement.classList.toggle('dark')}
      className='bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
    >
      Toggle
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Dark mode, theme persistence, and the full Tailwind + CSS variable setup are included in the AI SaaS Starter Kit — ships with both light and dark themes out of the box.

Top comments (0)