Implementing Dark Mode in React Applications
Users expect dark mode in applications. It reduces eye strain in low light and saves battery on OLED screens. Some users prefer dark interfaces all the time.
Adding dark mode to React applications involves managing theme state, applying styles conditionally, and persisting user preferences.
Basic approach with CSS variables
CSS variables make theme switching straightforward. Define colors for both themes:
/* styles/themes.css */
:root {
--bg-primary: #ffffff;
--bg-secondary: #f3f4f6;
--text-primary: #111827;
--text-secondary: #6b7280;
--border: #e5e7eb;
}
[data-theme="dark"] {
--bg-primary: #1f2937;
--bg-secondary: #111827;
--text-primary: #f9fafb;
--text-secondary: #9ca3af;
--border: #374151;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
}
Components reference these variables:
.card {
background-color: var(--bg-secondary);
border: 1px solid var(--border);
color: var(--text-primary);
}
Toggle the theme by changing a data attribute on the root element.
Theme context
Create a context to manage theme state:
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);
document.documentElement.setAttribute('data-theme', savedTheme);
}
}, []);
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
};
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;
}
Wrap your app with the provider:
import { ThemeProvider } from './ThemeContext';
function App() {
return (
<ThemeProvider>
<YourApp />
</ThemeProvider>
);
}
Theme toggle button
Create a button to switch themes:
import { useTheme } from './ThemeContext';
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme} aria-label="Toggle theme">
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
}
Users can click this to switch between light and dark mode.
Respecting system preferences
Check the users system preference on first load:
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() => {
// Check localStorage first
const savedTheme = localStorage.getItem('theme');
if (savedTheme) return savedTheme;
// Fall back to system preference
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
});
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
// ... rest of provider
}
This defaults to the users system setting if they havent chosen a preference in your app.
Listening for system changes
Users might change their system theme while using your app:
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e) => {
// Only update if user hasnt set a preference
if (!localStorage.getItem('theme')) {
setTheme(e.matches ? 'dark' : 'light');
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
This keeps the app in sync with system changes unless the user explicitly chose a theme.
Avoiding flash of wrong theme
Without proper handling users see a flash of light theme before dark mode loads. This happens because JavaScript runs after the page renders.
Add a script in your HTML head before React loads:
<!DOCTYPE html>
<html>
<head>
<script>
(function() {
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
<!-- rest of head -->
</head>
This sets the theme before any content renders.
Styled components approach
If you use styled-components the pattern is similar:
import { ThemeProvider } from 'styled-components';
const lightTheme = {
bg: '#ffffff',
text: '#000000',
};
const darkTheme = {
bg: '#1f2937',
text: '#f9fafb',
};
function App() {
const [isDark, setIsDark] = useState(false);
return (
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<GlobalStyles />
<YourApp />
</ThemeProvider>
);
}
Components access theme values through props:
const Card = styled.div`
background: ${props => props.theme.bg};
color: ${props => props.theme.text};
`;
Images and media
Some images dont work well in dark mode. Provide alternatives:
function Logo() {
const { theme } = useTheme();
return (
<img
src={theme === 'light' ? '/logo-dark.svg' : '/logo-light.svg'}
alt="Logo"
/>
);
}
Or use CSS filters:
[data-theme="dark"] img {
filter: brightness(0.8) contrast(1.2);
}
This works for photos but might not be appropriate for logos or illustrations.
Tailwind CSS approach
With Tailwind use the dark variant:
<div className="bg-white dark:bg-gray-900 text-black dark:text-white">
Content
</div>
Configure Tailwind to use a class strategy:
// tailwind.config.js
module.exports = {
darkMode: 'class',
// ...
}
Then add/remove the dark class on the root element instead of using a data attribute.
Testing both themes
Test your application in both themes during development. Some issues only appear in one theme:
- Low contrast text
- Invisible borders
- Images that dont work in dark mode
- Hardcoded colors that override theme
Go through major user flows in both themes to catch these issues.
Performance considerations
Theme switching should be instant. Don't make API calls or do expensive calculations when toggling themes.
Keep theme state separate from other application state. Users might switch themes frequently and it shouldnt affect other parts of the app.
Common mistakes
Forgetting to theme all components. Some elements might use hardcoded colors instead of theme variables.
Not testing in both themes. Issues in dark mode often go unnoticed if you only test in light mode.
Making dark mode just an inverted light mode. Dark themes need adjusted colors not just black backgrounds.
Ignoring user preferences. Always respect system preferences as the default.
Summary
Dark mode improves user experience and is expected in modern applications. Use CSS variables for theme colors, Context API for state management, and localStorage for persistence.
Respect system preferences but let users override them. Avoid flash of wrong theme with inline scripts. Test thoroughly in both themes to catch contrast and visibility issues.