Dark Mode Toggle in React
Quick Notes
- Use global class to indicate when dark mode is enabled
- For SPAs, the global class can be applied at the root component level
- For NextJS, set the global class on the body in the
_document.tsx
- When using styled components, leverage the
ThemeProvider
- Set the default dark/light mode based on the
prefers-color-scheme
media query to honor user's preference- See related: Using Users' Preferences for Dark Mode
- When the mode is changed, store the user's selection in
localStorage
Examples
Set the initial Mode
// Constants for the dark mode CSS class and local storage key
export const DARK_MODE_CLASS = 'dark_mode';
export const DARK_MODE_STORAGE_KEY = 'dark_mode';
if (
localStorage?.getItem(DARK_MODE_STORAGE_KEY}) ??
window.matchMedia('(prefers-color-scheme: dark)').matches
) {
document.body.classList.add(DARK_MODE_CLASS);
}
To ensure this happens as early as possible in NextJS:
File:
_document.tsx
class CustomDocument extends Document {
render() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
<Script id="dark_mode_init" strategy="afterInteractive">
{`
if (
localStorage?.getItem('${DARK_MODE_STORAGE_KEY}') ??
window.matchMedia('(prefers-color-scheme: dark)').matches
) {
document.body.classList.add('${DARK_MODE_CLASS}');
}
`}
</Script>
</body>
</Html>
);
}
}
Toggling the Mode
// Constants for the dark mode CSS class and local storage key
export const DARK_MODE_CLASS = 'dark_mode';
export const DARK_MODE_STORAGE_KEY = 'dark_mode';
export function toggleDarkMode() {
if (document.body.classList.contains(DARK_MODE_CLASS)) {
document.body.classList.remove(DARK_MODE_CLASS);
localStorage?.setItem(DARK_MODE_STORAGE_KEY, '');
} else {
document.body.classList.add(DARK_MODE_CLASS);
localStorage?.setItem(DARK_MODE_STORAGE_KEY, 'true');
}
}
Hook Tracking the Mode in state using a MutationObserver
export default function useIsDarkMode() {
const [isDarkMode, setIsDarkMode] = useState<boolean>(false);
useEffect(() => {
const observer = new MutationObserver(() => {
setIsDarkMode(document.body.classList.contains(DARK_MODE_CLASS));
});
observer.observe(document.body, {
attributes: true,
attributeFilter: ['class'],
characterData: false,
childList: false,
subtree: false,
});
// Set the initial value
// For NextJS this has to be done within the effect since
// it must be run client side - for SSR projects it can be done
// in the state initialization
setIsDarkMode(document.body.classList.contains(DARK_MODE_CLASS));
return () => {
observer.disconnect();
};
}, []);
return isDarkMode;
}
Applying Styles Using CSS Variables
body {
--background-color: #ffffff;
--text-color: #000000;
/**
* The variables can be used anywhere below body
* (even in components using css modules)
*/
color: var(--text-color);
background-color: var(--background-color);
}
body.dark_mode {
--background-color: #000000;
--text-color: #ffffff;
}
Applying Styles Directly in CSS/SCSS Modules
.my_component {
// Light mode styles
background-color: #ffffff;
color: #000000;
}
:global(body.dark_mode) {
.my_component {
/** Dark mode styles */
background-color: #ffffff;
color: #000000;
}
}