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
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
Step 1: CSS Custom Properties
Define your color scheme using CSS variables. This makes it easy to switch between themes by changing variable values.
Step 2: Respect System Preferences
Use the prefers-color-scheme media query to automatically detect and apply the user's system theme preference.
Step 5: Basic JavaScript Toggle
Add a simple JavaScript function to toggle the dark class on the document element.
Step 6: Persist with localStorage
Save the user's theme preference to localStorage so it persists across page reloads and sessions.
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.
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
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.
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.
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.
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.
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.
More CSS Tutorials
Copy CSS from Website
Extract styles with 2 clicks
Center a Div (CSS)
Flexbox, Grid, and more
Center a Div (Tailwind)
Utility classes for centering
Convert CSS to Tailwind
CSS to utility classes
Glassmorphism Effect
Frosted glass UI style
Neumorphism Button
Soft UI button design
Responsive Navbar
Mobile-friendly navigation
Sticky Header
Fixed navigation on scroll
CSS Pagination
Page navigation styles
CSS Accordion
Expandable content sections
Dropdown Menu
Hover and click dropdowns
CSS Modal
Popup dialog boxes
Toast Notifications
Animated alert messages
Hamburger Menu
Animated menu icon
Animate Gradients
Moving gradient backgrounds
Skeleton Loader
Loading placeholder UI
Element Screenshot
Capture any element as image
Pick Color from Website
Eyedropper tool comparison
Identify Fonts
Find fonts on any website
Download All Images
Bulk save images from any site
Measure Elements
Page ruler and measurements
Extract Colors from Website
Get any color palette instantly
Responsive Grid
CSS Grid auto-fit and minmax
Center Text in CSS
text-align, Flexbox, and Grid
Make Text Bold
font-weight values explained
Add Shadow in CSS
box-shadow, text-shadow, drop-shadow
Round Corners in CSS
border-radius and pill shapes
Style File Input
Custom upload buttons
