Implementing Dark Mode in React
Dark mode has become an essential feature for modern web applications. Let’s explore how to implement it properly with React.
Why Dark Mode Matters#
- Reduces eye strain in low-light environments
- Saves battery on OLED screens
- User preference and accessibility
- Modern, polished feel
CSS Variables Example#
Define theme colors as CSS variables:
:root {
--bg-primary: #ffffff;
--bg-secondary: #f3f4f6;
--text-primary: #111827;
--text-secondary: #6b7280;
--border: #d1d5db;
}
[data-theme='dark'] {
--bg-primary: #1f2937;
--bg-secondary: #374151;
--text-primary: #f3f4f6;
--text-secondary: #9ca3af;
--border: #4b5563;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease;
}React Context Implementation#
import { createContext, useContext, useState, useEffect } from 'react'
const ThemeContext = createContext()
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
useEffect(() => {
// Load saved theme
const savedTheme = localStorage.getItem('theme')
if (savedTheme) {
setTheme(savedTheme)
} else {
// Use system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
setTheme(prefersDark ? 'dark' : 'light')
}
}, [])
useEffect(() => {
// Apply theme to document
document.documentElement.setAttribute('data-theme', theme)
localStorage.setItem('theme', theme)
}, [theme])
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}Usage in Components#
function Header() {
const { theme, toggleTheme } = useTheme()
return (
<header>
<h1>My App</h1>
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙' : '☀️'}
</button>
</header>
)
}Detecting System Preference#
Listen to system theme changes:
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleChange = (e) => {
if (e.matches) {
setTheme('dark')
} else {
setTheme('light')
}
}
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
}, [])Preventing Flash of Unstyled Content#
Add a script in your HTML <head> to set theme before render:
<script>
(function() {
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
document.documentElement.setAttribute('data-theme', theme)
})()
</script>TailwindCSS Dark Mode#
If using Tailwind, enable dark mode in config:
// tailwind.config.js
module.exports = {
darkMode: 'class', // or 'media' for system preference
// ... other config
}Usage:
<div className="bg-white dark:bg-gray-900 text-black dark:text-white">
This text changes color based on theme
</div>Best Practices#
- Respect user preference - Check system settings first
- Persist choice - Save to localStorage
- Smooth transitions - Use CSS transitions for theme changes
- Test contrast ratios - Ensure accessibility in both modes
- Consider images - Provide different versions for each theme if needed
Conclusion#
Dark mode is more than just inverting colors. A thoughtful implementation respects user preferences, maintains accessibility, and enhances the overall user experience!