Custom useDarkMode Hook: Dark Theme Toggle Made Right (2026)
Dark mode has moved from a nice-to-have feature to an expectation. Users expect applications to respect their system preferences, remember their choice, and switch themes instantly without page flicker. Implementing this correctly requires handling multiple concerns: system preference detection, localStorage persistence, server-side rendering, and real-time updates across components.
A custom useDarkMode hook encapsulates all this complexity, giving you a clean interface to toggle themes throughout your application. In this guide, we'll build a production-ready implementation that handles edge cases, respects user preferences, and works seamlessly in modern React applications.
Table of Contents
- Understanding Dark Mode Requirements
- Basic useDarkMode Hook Implementation
- Detecting System Preferences
- Persisting User Choice with localStorage
- Server-Side Rendering Considerations
- Integration with React Context
- Advanced: Real-Time System Preference Changes
- Practical Example: Theme Provider Setup
- FAQ
Understanding Dark Mode Requirements
Before building, let's clarify what a robust dark mode solution needs:
1. System Preference Detection
Modern browsers support prefers-color-scheme media query. You should detect whether the user's OS is set to dark mode. This is the first fallback when the user hasn't explicitly chosen a theme.
2. User Choice Persistence
When a user toggles dark mode in your app, their choice should be remembered across sessions. localStorage is the standard approach for this, though some apps prefer URL parameters or server-side storage.
3. No Flash of Wrong Theme
The worst user experience is when the page loads in light mode, then flashes to dark mode. This happens when your theme detection happens after the initial render. You need to detect the theme synchronously or use special techniques to load the theme before rendering.
4. Responsive to System Changes
If the user changes their OS theme preference, your app should update immediately. You'll use the change event on the prefers-color-scheme media query listener to detect this.
5. Server-Side Rendering Safety
In Next.js or other SSR frameworks, localStorage isn't available on the server. You need to handle this gracefully without throwing errors.
6. Performance
Switching themes should update the DOM instantly without layout shifts. Ideally, you're just toggling a class or CSS variable, not re-rendering the entire component tree.
Basic useDarkMode Hook Implementation
Here's a straightforward version that gets you started:
TypeScript Version
import { useState } from 'react';
interface UseDarkModeReturn {
isDarkMode: boolean;
toggle: () => void;
}
export function useDarkMode(): UseDarkModeReturn {
const [isDarkMode, setIsDarkMode] = useState(false);
const toggle = () => {
setIsDarkMode(prev => !prev);
};
return { isDarkMode, toggle };
}
JavaScript Version
import { useState } from 'react';
export function useDarkMode() {
const [isDarkMode, setIsDarkMode] = useState(false);
const toggle = () => {
setIsDarkMode(prev => !prev);
};
return { isDarkMode, toggle };
}
You can use this in a component like this:
TypeScript Component Usage
export function App() {
const { isDarkMode, toggle } = useDarkMode();
return (
<div className={isDarkMode ? 'dark' : 'light'}>
<button onClick={toggle}>
{isDarkMode ? '☀️ Light' : '🌙 Dark'}
</button>
<p>Current theme: {isDarkMode ? 'Dark' : 'Light'}</p>
</div>
);
}
This basic version works but misses all the real-world requirements. Let's progressively add them.
Detecting System Preferences
The first improvement is respecting the user's system setting. The window.matchMedia() API lets you check and listen to the prefers-color-scheme media query:
TypeScript Version (With System Detection)
import { useState, useEffect } from 'react';
interface UseDarkModeReturn {
isDarkMode: boolean;
toggle: () => void;
}
export function useDarkMode(): UseDarkModeReturn {
// Default to system preference
const [isDarkMode, setIsDarkMode] = useState(() => {
// Check if we're in the browser
if (typeof window === 'undefined') {
return false;
}
// Check system preference
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
return isDark;
});
const toggle = () => {
setIsDarkMode(prev => !prev);
};
// Listen for system preference changes
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
setIsDarkMode(e.matches);
};
// addEventListener is the modern approach
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, []);
return { isDarkMode, toggle };
}
JavaScript Version (With System Detection)
import { useState, useEffect } from 'react';
export function useDarkMode() {
const [isDarkMode, setIsDarkMode] = useState(() => {
if (typeof window === 'undefined') {
return false;
}
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
return isDark;
});
const toggle = () => {
setIsDarkMode(prev => !prev);
};
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e) => {
setIsDarkMode(e.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, []);
return { isDarkMode, toggle };
}
Key points here:
- The
typeof window === 'undefined'check prevents errors in SSR environments window.matchMedia()returns aMediaQueryListobject- We use
addEventListener(modern) instead of the deprecatedaddListener - The cleanup function removes the listener to prevent memory leaks
- System preference changes automatically update the state
Persisting User Choice with localStorage
Now let's add localStorage persistence so the user's explicit choice is remembered:
TypeScript Version (With Persistence)
import { useState, useEffect } from 'react';
const DARK_MODE_KEY = 'app-dark-mode-preference';
interface UseDarkModeReturn {
isDarkMode: boolean;
toggle: () => void;
}
export function useDarkMode(): UseDarkModeReturn {
// Initialize from localStorage or system preference
const [isDarkMode, setIsDarkMode] = useState(() => {
if (typeof window === 'undefined') {
return false;
}
// Check localStorage first (user's explicit choice)
const stored = localStorage.getItem(DARK_MODE_KEY);
if (stored !== null) {
return stored === 'true';
}
// Fall back to system preference
return window.matchMedia('(prefers-color-scheme: dark)').matches;
});
// Persist to localStorage whenever dark mode changes
useEffect(() => {
localStorage.setItem(DARK_MODE_KEY, String(isDarkMode));
}, [isDarkMode]);
// Listen for system preference changes (only if no user preference stored)
useEffect(() => {
if (typeof window === 'undefined') return;
const stored = localStorage.getItem(DARK_MODE_KEY);
// Only listen to system changes if user hasn't made explicit choice
if (stored !== null) return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
setIsDarkMode(e.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, []);
const toggle = () => {
setIsDarkMode(prev => !prev);
};
return { isDarkMode, toggle };
}
JavaScript Version (With Persistence)
import { useState, useEffect } from 'react';
const DARK_MODE_KEY = 'app-dark-mode-preference';
export function useDarkMode() {
const [isDarkMode, setIsDarkMode] = useState(() => {
if (typeof window === 'undefined') {
return false;
}
const stored = localStorage.getItem(DARK_MODE_KEY);
if (stored !== null) {
return stored === 'true';
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
});
useEffect(() => {
localStorage.setItem(DARK_MODE_KEY, String(isDarkMode));
}, [isDarkMode]);
useEffect(() => {
if (typeof window === 'undefined') return;
const stored = localStorage.getItem(DARK_MODE_KEY);
if (stored !== null) return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e) => {
setIsDarkMode(e.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, []);
const toggle = () => {
setIsDarkMode(prev => !prev);
};
return { isDarkMode, toggle };
}
Important behavior here:
- localStorage takes priority over system preference
- Once a user explicitly toggles the theme, we stop listening to system changes
- If you wanted to reset to system preference, you'd clear localStorage
- The key is stored as a string ('true'/'false') because localStorage only stores strings
Server-Side Rendering Considerations
In a Next.js or Remix application, localStorage doesn't exist on the server. We need special handling to prevent hydration mismatches:
TypeScript Version (SSR-Safe)
import { useState, useEffect } from 'react';
const DARK_MODE_KEY = 'app-dark-mode-preference';
interface UseDarkModeReturn {
isDarkMode: boolean;
toggle: () => void;
isLoaded: boolean;
}
export function useDarkMode(): UseDarkModeReturn {
// Initialize to false, will hydrate on client
const [isDarkMode, setIsDarkMode] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
// This effect only runs on the client, after hydration
useEffect(() => {
// Determine the actual theme
let theme = false;
if (typeof window !== 'undefined') {
const stored = localStorage.getItem(DARK_MODE_KEY);
if (stored !== null) {
theme = stored === 'true';
} else {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches;
}
}
// Update state only after client hydration is complete
setIsDarkMode(theme);
setIsLoaded(true);
// Set up system preference listener
if (typeof window !== 'undefined') {
const stored = localStorage.getItem(DARK_MODE_KEY);
if (stored === null) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
setIsDarkMode(e.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}
}
}, []);
// Persist to localStorage
useEffect(() => {
if (typeof window !== 'undefined' && isLoaded) {
localStorage.setItem(DARK_MODE_KEY, String(isDarkMode));
}
}, [isDarkMode, isLoaded]);
const toggle = () => {
setIsDarkMode(prev => !prev);
};
return { isDarkMode, toggle, isLoaded };
}
JavaScript Version (SSR-Safe)
import { useState, useEffect } from 'react';
const DARK_MODE_KEY = 'app-dark-mode-preference';
export function useDarkMode() {
const [isDarkMode, setIsDarkMode] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
let theme = false;
if (typeof window !== 'undefined') {
const stored = localStorage.getItem(DARK_MODE_KEY);
if (stored !== null) {
theme = stored === 'true';
} else {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches;
}
}
setIsDarkMode(theme);
setIsLoaded(true);
if (typeof window !== 'undefined') {
const stored = localStorage.getItem(DARK_MODE_KEY);
if (stored === null) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e) => {
setIsDarkMode(e.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}
}
}, []);
useEffect(() => {
if (typeof window !== 'undefined' && isLoaded) {
localStorage.setItem(DARK_MODE_KEY, String(isDarkMode));
}
}, [isDarkMode, isLoaded]);
const toggle = () => {
setIsDarkMode(prev => !prev);
};
return { isDarkMode, toggle, isLoaded };
}
SSR-specific improvements:
- The
isLoadedflag prevents rendering content before hydration completes - Initialization happens on the client only, not the server
- No localStorage access during server rendering
- Components can use
isLoadedto avoid rendering theme-dependent content before hydration
In your component:
export function App() {
const { isDarkMode, toggle, isLoaded } = useDarkMode();
// Skip rendering until theme is loaded (prevents flash)
if (!isLoaded) {
return null; // Or a skeleton
}
return (
<div className={isDarkMode ? 'dark' : 'light'}>
<button onClick={toggle}>Toggle Theme</button>
</div>
);
}
Integration with React Context
For a real application, you want all components to access the theme without prop drilling. React Context is the perfect pattern:
TypeScript Implementation
import { ReactNode, createContext, useContext } from 'react';
import { useDarkMode as useDarkModeHook } from './useDarkMode';
interface ThemeContextType {
isDarkMode: boolean;
toggle: () => void;
isLoaded: boolean;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
interface ThemeProviderProps {
children: ReactNode;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
const darkMode = useDarkModeHook();
return (
<ThemeContext.Provider value={darkMode}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme(): ThemeContextType {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
JavaScript Implementation
import { createContext, useContext } from 'react';
import { useDarkMode as useDarkModeHook } from './useDarkMode';
const ThemeContext = createContext(undefined);
export function ThemeProvider({ children }) {
const darkMode = useDarkModeHook();
return (
<ThemeContext.Provider value={darkMode}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
Now components can access the theme anywhere:
export function Header() {
const { isDarkMode, toggle } = useTheme();
return (
<header className={isDarkMode ? 'dark' : 'light'}>
<h1>My App</h1>
<button onClick={toggle}>Toggle</button>
</header>
);
}
export function Footer() {
const { isDarkMode } = useTheme();
return (
<footer className={isDarkMode ? 'dark' : 'light'}>
<p>© 2025 My Company</p>
</footer>
);
}
And in your main app:
export default function App() {
return (
<ThemeProvider>
<Header />
<main>
<YourContent />
</main>
<Footer />
</ThemeProvider>
);
}
Advanced: Real-Time System Preference Changes
For production applications, handle the edge case where the system preference changes while the app is open:
TypeScript Implementation (Advanced)
import { useState, useEffect, useCallback } from 'react';
const DARK_MODE_KEY = 'app-dark-mode-preference';
type ThemeMode = 'light' | 'dark' | 'system';
interface UseDarkModeReturn {
isDarkMode: boolean;
toggle: () => void;
setThemeMode: (mode: ThemeMode) => void;
themeMode: ThemeMode;
isLoaded: boolean;
}
export function useDarkMode(): UseDarkModeReturn {
const [isDarkMode, setIsDarkMode] = useState(false);
const [themeMode, setThemeModeState] = useState<ThemeMode>('system');
const [isLoaded, setIsLoaded] = useState(false);
// Determine if system is in dark mode
const getSystemPreference = useCallback(() => {
if (typeof window === 'undefined') return false;
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}, []);
// Initialize theme on client
useEffect(() => {
const stored = localStorage.getItem(DARK_MODE_KEY) as ThemeMode | null;
const mode: ThemeMode = stored || 'system';
setThemeModeState(mode);
let theme = false;
if (mode === 'dark') {
theme = true;
} else if (mode === 'light') {
theme = false;
} else {
theme = getSystemPreference();
}
setIsDarkMode(theme);
setIsLoaded(true);
}, [getSystemPreference]);
// Listen to system changes only in 'system' mode
useEffect(() => {
if (themeMode !== 'system' || !isLoaded) return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
setIsDarkMode(e.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, [themeMode, isLoaded]);
// Persist theme mode and update dark mode state
const setThemeMode = useCallback((mode: ThemeMode) => {
setThemeModeState(mode);
localStorage.setItem(DARK_MODE_KEY, mode);
if (mode === 'dark') {
setIsDarkMode(true);
} else if (mode === 'light') {
setIsDarkMode(false);
} else {
setIsDarkMode(getSystemPreference());
}
}, [getSystemPreference]);
const toggle = useCallback(() => {
setThemeMode(isDarkMode ? 'light' : 'dark');
}, [isDarkMode, setThemeMode]);
return { isDarkMode, toggle, setThemeMode, themeMode, isLoaded };
}
JavaScript Implementation (Advanced)
import { useState, useEffect, useCallback } from 'react';
const DARK_MODE_KEY = 'app-dark-mode-preference';
export function useDarkMode() {
const [isDarkMode, setIsDarkMode] = useState(false);
const [themeMode, setThemeModeState] = useState('system');
const [isLoaded, setIsLoaded] = useState(false);
const getSystemPreference = useCallback(() => {
if (typeof window === 'undefined') return false;
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}, []);
useEffect(() => {
const stored = localStorage.getItem(DARK_MODE_KEY);
const mode = stored || 'system';
setThemeModeState(mode);
let theme = false;
if (mode === 'dark') {
theme = true;
} else if (mode === 'light') {
theme = false;
} else {
theme = getSystemPreference();
}
setIsDarkMode(theme);
setIsLoaded(true);
}, [getSystemPreference]);
useEffect(() => {
if (themeMode !== 'system' || !isLoaded) return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e) => {
setIsDarkMode(e.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, [themeMode, isLoaded]);
const setThemeMode = useCallback((mode) => {
setThemeModeState(mode);
localStorage.setItem(DARK_MODE_KEY, mode);
if (mode === 'dark') {
setIsDarkMode(true);
} else if (mode === 'light') {
setIsDarkMode(false);
} else {
setIsDarkMode(getSystemPreference());
}
}, [getSystemPreference]);
const toggle = useCallback(() => {
setThemeMode(isDarkMode ? 'light' : 'dark');
}, [isDarkMode, setThemeMode]);
return { isDarkMode, toggle, setThemeMode, themeMode, isLoaded };
}
This advanced version lets users choose between three options:
- 'light' - Always light mode
- 'dark' - Always dark mode
- 'system' - Follow OS preference and update when it changes
Practical Example: Theme Provider Setup
Here's a complete, production-ready setup for a Next.js or React app:
TypeScript Complete Setup
// hooks/useDarkMode.ts
import { useState, useEffect, useCallback } from 'react';
const DARK_MODE_KEY = 'app-dark-mode-preference';
type ThemeMode = 'light' | 'dark' | 'system';
interface UseDarkModeReturn {
isDarkMode: boolean;
toggle: () => void;
setThemeMode: (mode: ThemeMode) => void;
themeMode: ThemeMode;
isLoaded: boolean;
}
export function useDarkMode(): UseDarkModeReturn {
const [isDarkMode, setIsDarkMode] = useState(false);
const [themeMode, setThemeModeState] = useState<ThemeMode>('system');
const [isLoaded, setIsLoaded] = useState(false);
const getSystemPreference = useCallback(() => {
if (typeof window === 'undefined') return false;
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}, []);
useEffect(() => {
const stored = localStorage.getItem(DARK_MODE_KEY) as ThemeMode | null;
const mode: ThemeMode = stored || 'system';
setThemeModeState(mode);
let theme = false;
if (mode === 'dark') {
theme = true;
} else if (mode === 'light') {
theme = false;
} else {
theme = getSystemPreference();
}
setIsDarkMode(theme);
setIsLoaded(true);
}, [getSystemPreference]);
useEffect(() => {
if (themeMode !== 'system' || !isLoaded) return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
setIsDarkMode(e.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, [themeMode, isLoaded]);
const setThemeMode = useCallback((mode: ThemeMode) => {
setThemeModeState(mode);
localStorage.setItem(DARK_MODE_KEY, mode);
if (mode === 'dark') {
setIsDarkMode(true);
} else if (mode === 'light') {
setIsDarkMode(false);
} else {
setIsDarkMode(getSystemPreference());
}
}, [getSystemPreference]);
const toggle = useCallback(() => {
setThemeMode(isDarkMode ? 'light' : 'dark');
}, [isDarkMode, setThemeMode]);
return { isDarkMode, toggle, setThemeMode, themeMode, isLoaded };
}
// contexts/ThemeContext.tsx
import { ReactNode, createContext, useContext } from 'react';
import { useDarkMode } from '@/hooks/useDarkMode';
interface ThemeContextType {
isDarkMode: boolean;
toggle: () => void;
setThemeMode: (mode: 'light' | 'dark' | 'system') => void;
themeMode: 'light' | 'dark' | 'system';
isLoaded: boolean;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
interface ThemeProviderProps {
children: ReactNode;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
const darkMode = useDarkMode();
return (
<ThemeContext.Provider value={darkMode}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme(): ThemeContextType {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// components/ThemeToggle.tsx
import { useTheme } from '@/contexts/ThemeContext';
export function ThemeToggle() {
const { isDarkMode, toggle, themeMode, setThemeMode } = useTheme();
return (
<div className="flex items-center gap-2">
<button
onClick={toggle}
className="px-3 py-2 rounded-lg bg-gray-200 dark:bg-gray-700"
aria-label="Toggle theme"
>
{isDarkMode ? '☀️' : '🌙'}
</button>
<select
value={themeMode}
onChange={(e) => setThemeMode(e.target.value as any)}
className="px-3 py-2 rounded-lg bg-gray-200 dark:bg-gray-700"
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
</div>
);
}
// app/layout.tsx (Next.js example)
import { ThemeProvider } from '@/contexts/ThemeContext';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
}
CSS with dark mode support
/* styles/globals.css */
:root {
--background: #ffffff;
--foreground: #000000;
}
html.dark {
--background: #000000;
--foreground: #ffffff;
}
body {
background-color: var(--background);
color: var(--foreground);
}
/* Or using Tailwind CSS (simpler approach) */
/* Just use dark: prefix in your classes */
FAQ
Q: Should I store the theme in localStorage or URL query parameters?
A: localStorage is better for most cases. It persists across sessions and doesn't clutter URLs. Query parameters are useful if you want to share a specific theme preference with others (e.g., ?theme=dark), but that's rare. If you need both, you can support both: check URL first, fall back to localStorage, then system preference.
Q: How do I prevent the flash of wrong theme in Next.js?
A: The isLoaded flag helps, but for zero flash, you need to inject a script in the HTML <head> that runs before React hydrates:
// lib/theme-script.ts
export const themeScript = `
(function() {
const theme = localStorage.getItem('app-dark-mode-preference') || 'system';
let isDark = false;
if (theme === 'dark') {
isDark = true;
} else if (theme === 'system') {
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
}
if (isDark) {
document.documentElement.classList.add('dark');
}
})();
`;
Then in your Next.js layout:
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
Q: What about syncing theme across browser tabs?
A: Use the storage event to listen to localStorage changes in other tabs:
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === DARK_MODE_KEY && e.newValue) {
setThemeMode(e.newValue as any);
}
};
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []);
Q: Can I use CSS color-scheme property instead of classes?
A: Yes! color-scheme is more semantic and affects form elements:
html {
color-scheme: light;
}
html.dark {
color-scheme: dark;
}
This automatically styles inputs, buttons, and other form elements appropriately for each theme.
Q: What about high contrast mode users?
A: Respect prefers-contrast in addition to prefers-color-scheme:
const hasHighContrast = window.matchMedia('(prefers-contrast: more)').matches;
You can adjust your color palette accordingly to ensure sufficient contrast.
Q: Does useIsMounted apply here? Should I check if mounted before setting theme?
A: No need. The theme detection happens in useEffect, which only runs on the client after mounting. Setting localStorage is safe because localStorage doesn't cause memory leak warnings like state updates do.
Related Articles
- React Context API: Sharing State Across Components
- localStorage and SessionStorage in React
- Server-Side Rendering with React: Hydration and Pitfalls
- CSS Variables in React: Dynamic Styling Made Simple
- Building Accessible Components with prefers-color-scheme
Questions? Share how you've implemented dark mode in your projects. Do you prefer system preference detection, user override, or something else? What edge cases have you encountered? Leave your thoughts in the comments!
Google AdSense Placeholder
CONTENT SLOT