Skip to main content

Last updated: March 13, 2026

How to Create a Dark Mode Toggle with CSS

The complete guide to implementing dark mode with CSS custom properties, system preference detection, and localStorage persistence.

Why Dark Mode Matters

Dark mode is not just a trend - it reduces eye strain in low-light conditions, saves battery on OLED screens, and many users simply prefer it. A good dark mode implementation respects user preferences while giving them manual control.

CSS Variables

Define your color palette once and switch themes by changing variable values.

System Detection

Use prefers-color-scheme to automatically match the user's OS preference.

Persistence

Save user's choice to localStorage so it persists across sessions.

Interactive Demo

Try the toggle:

Sample Card

This card changes appearance based on the selected theme. Notice how the background, text, and border colors all update smoothly.

Current theme: light

1

Step 1: CSS Custom Properties

Define your color scheme using CSS variables. This makes it easy to switch between themes by changing variable values.

2

Step 2: Respect System Preferences

Use the prefers-color-scheme media query to automatically detect and apply the user's system theme preference.

3

Step 3: Toggle Button HTML

Create an accessible toggle button with proper ARIA attributes and icons for both themes.

4

Step 4: Toggle Button Styles

Style the toggle button and show/hide the appropriate icon based on the current theme.

5

Step 5: Basic JavaScript Toggle

Add a simple JavaScript function to toggle the dark class on the document element.

6

Step 6: Persist with localStorage

Save the user's theme preference to localStorage so it persists across page reloads and sessions.

7

Step 7: Prevent Flash of Wrong Theme

Add this script in the <head> before your CSS to prevent a flash of the default theme on page load.

8

Complete Working Example

A full, copy-paste ready implementation combining all the concepts above.

Key Concepts

CSS Custom Properties

CSS variables (custom properties) are the foundation of modern theming. Define them once in :root and override them in your .dark class.

:root { --bg: white; }
.dark { --bg: #111; }

prefers-color-scheme

This media query detects the user's system preference. Use it as the default, but allow manual override.

@media (prefers-color-scheme: dark) {
  :root { --bg: #111; }
}

localStorage Persistence

Save the user's manual choice to localStorage. Check it on page load before falling back to system preference.

localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');

Preventing Flash

Add a blocking script in the head that applies the theme before CSS loads. This prevents a flash of the wrong theme.

<script>
  document.documentElement
    .classList.add(getTheme());
</script>

Best Practices

Do

  • +Use CSS custom properties for all theme-dependent colors
  • +Respect prefers-color-scheme as the default
  • +Save user preference to localStorage
  • +Add smooth transitions for theme changes
  • +Include proper ARIA labels on toggle buttons

Do Not

  • -Hardcode colors throughout your CSS
  • -Ignore system preferences entirely
  • -Flash the wrong theme on page load
  • -Use cookies instead of localStorage for theme storage
  • -Forget about contrast ratios in dark mode

Framework-Specific Tips

Tailwind CSS

Tailwind has built-in dark mode support. Use the dark: variant prefix and configure darkMode in tailwind.config.js.

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  // or 'media' for system
}

React

Use a context provider to manage theme state globally. Consider using useSyncExternalStore for localStorage sync.

const ThemeContext =
  createContext('light');

<ThemeContext.Provider
  value={theme}>
  ...
</ThemeContext.Provider>

Next.js

Use next-themes package for SSR-safe dark mode with automatic flash prevention.

import { ThemeProvider }
  from 'next-themes';

<ThemeProvider
  attribute="class">
  ...
</ThemeProvider>

Frequently Asked Questions

What is the best way to implement dark mode?

The best approach combines CSS custom properties (variables) for theming, the prefers-color-scheme media query to respect system preferences, and JavaScript with localStorage to remember user choices. This gives users control while respecting their default preferences.

How do I make dark mode persist across page reloads?

Use localStorage to save the user's theme preference. When the page loads, check localStorage first, then fall back to the system preference using prefers-color-scheme. Apply the saved theme before the page renders to avoid a flash of wrong theme.

Should I use a class or data attribute for dark mode?

Both work well. Using a class like 'dark' on the html or body element is common and works great with Tailwind CSS. Data attributes like data-theme='dark' are semantic and can be more descriptive. Choose based on your framework and preferences.

How do I prevent flash of wrong theme on page load?

Add a script in the head of your HTML (before CSS loads) that reads the theme from localStorage and applies it immediately. This runs synchronously before the page renders, preventing any flash of the default theme.

Can I implement dark mode with CSS only?

Yes! Using prefers-color-scheme media query, you can implement a CSS-only dark mode that follows the user's system preference. However, this won't let users manually toggle the theme. For a toggle button, you'll need a small amount of JavaScript.