Dark Mode Toggle in React
Dark mode is a theme selector that enhances user engagement by respecting their preferred theme.
Overview
In this blog, we will cover how to toggle between two themes: light and dark. (Multi-theme selection will be covered in upcoming blogs.)
UI Component
We will create a reusable UI component, a button that can be placed anywhere in the application while maintaining the same functionality.
Toggle Button UI
<div
className="relative w-14 h-8 rounded-3xl bg-slate-300 dark:bg-zinc-700 place-center cursor-pointer"
>
<div
className="absolute top-1 rounded-full w-6 h-6 bg-blue-500"
/>
</div>
In global.css
or index.css
The following styles help integrate Tailwind CSS and define a custom variant for dark mode:
@import 'tailwindcss';
/* Define a custom variant for dark mode */
@custom-variant dark (&:is(.dark *));
-
@import 'tailwindcss';
Loads Tailwind CSS into your global styles. -
@custom-variant dark (&:is(.dark *));
- Creates a custom Tailwind variant named
dark
. - Uses
:is(.dark *)
to apply styles when any parent element has the.dark
class. - Ensures that child elements automatically inherit dark mode styles.
- Creates a custom Tailwind variant named
Creating a Custom Hook for Toggling Dark Mode
We will now create a custom hook, useDarkMode
, to handle dark mode toggling.
Custom Hook: useDarkMode
import { useState, useEffect, useCallback } from "react";
export default function useDarkMode() {
const [isDarkMode, setIsDarkMode] = useState<boolean>(() => {
const savedTheme = localStorage.getItem("theme");
if (savedTheme) {
return savedTheme === "dark";
}
return window.matchMedia("(prefers-color-scheme: dark)").matches;
});
// Toggle dark mode
const toggleDarkMode = useCallback(() => {
setIsDarkMode((prev) => {
const newMode = !prev;
const html = document.documentElement;
if (newMode) {
html.classList.add("dark");
localStorage.setItem("theme", "dark");
} else {
html.classList.remove("dark");
localStorage.setItem("theme", "light");
}
return newMode;
});
}, []);
// Sync the theme with the <html> element
useEffect(() => {
const html = document.documentElement;
if (isDarkMode) {
html.classList.add("dark");
} else {
html.classList.remove("dark");
}
}, [isDarkMode]);
return { isDarkMode, toggleDarkMode };
}
Explanation of useDarkMode
-
State Initialization: It first checks
localStorage
for a saved theme preference. If none exists, it defaults to the system theme preference. -
Toggling Theme: The
toggleDarkMode
function switches the theme, updates thehtml
class, and stores the new theme inlocalStorage
. -
Effect Hook: Ensures the theme remains synced with the
html
element when the component renders.
Using the Hook in a Component
Now, we will use useDarkMode
in a component to toggle dark mode.
ToggleDarkMode
Component
import useDarkMode from "../../../hooks/useDarkMode";
export default function ToggleDarkMode() {
const { toggleDarkMode, isDarkMode } = useDarkMode();
return (
<div
onClick={toggleDarkMode}
className="relative w-14 h-8 rounded-3xl bg-slate-300 dark:bg-zinc-700 place--center cursor-pointer"
>
<div
className={`absolute top-1 rounded-full w-6 h-6 bg-blue-500 ${isDarkMode ? "left-1" : "right-1"}`}
/>
</div>
);
}
Explanation
-
Imports
useDarkMode
Hook: ExtractstoggleDarkMode
andisDarkMode
from the hook. -
UI Structure: A switch-like button that changes its position based on
isDarkMode
. -
Click Handler: Calls
toggleDarkMode
to switch themes.
Using in Your App
Now you can import and use <ToggleDarkMode />
anywhere in your application!
Example: Using It in a Navbar
import ToggleDarkMode from "./components/ToggleDarkMode";
export default function Navbar() {
return (
<nav className="flex justify-between items-center p-4 bg-white dark:bg-black">
<h1 className="text-xl font-bold text-gray-900 dark:text-white">My Website</h1>
<ToggleDarkMode />
</nav>
);
}
Demo
Check out the live demo: Dark Mode Toggle
Top comments (4)
Hi It didnt work
Hey! Could you share more details on the issue? Is there a console error, or is the toggle not working as expected? Also, ensure Tailwind CSS v4.0 is set up correctly. Happy to help!
Could be due to the post was written in Typescript, thus useState can define the type -> useState'<'boolean'>'...
If you are writing in Javascript, just declare it as useState(...)
Nice