Hola folks! These days we all want to have the dark mode feature in our websites and applications. And why shouldn't we? It is more soothing to the eyes of the user, and we as UI/UX developers should tend to every need of our user.
But, how do we implement this dark mode feature in React? There are many things a developer is supposed to take care of while implementing this feature:
- User Preference š¤
- Use the system preference if the user is visiting for the first time.
- Use the user-preferred theme if the user has set it before.
- Store the user-preferred theme.
- Toggle theme preference š¤¹
- Users should be able to toggle between different themes.
- Avoiding
the Flicker
š¦- This flicker is eye-blinding and gives a bad user experience.
- Access to the theme š
- The theme should be easily accessible across the whole application.
Letās cater to the points mentioned above one by one & learn how to implement the dark mode feature in React.
User Preference
System-wide Theme Preference
Let us first try to access the userās system-wide theme preference. We can do this with the help of the prefers-color-scheme
media feature. We can pass this media feature with the theme values light
& dark
to know if the user has set any system-wide theme preference.
Now, we use the matchMedia
window method to check whether the document matches the passed media query string.
const preferColorSchemeQuery = "(prefers-color-scheme: dark)";
const theme = matchMedia(preferColorSchemeQuery).matches ? "dark" : "light";
User-preferred Theme
In a case where the user has already visited our application & has set some theme preference, we need to store this theme preference & retrieve it every time the user visits our application. We will use the local storage to store the userās theme preference.
localStorage.setItem("theme", "dark"); // or "light"
localStorage.getItem("theme");
This user-preferred theme is to be given priority over the system-wide theme preference. Therefore, the code will look as follows:
const preferColorSchemeQuery = "(prefers-color-scheme: dark)";
const theme = localStorage.getItem("theme") ||
(matchMedia(preferColorSchemeQuery).matches ? "dark" : "light");
Toggle theme preference
The user should be able to toggle between different themes. This feature can be easily provided with the help of a checkbox input & a theme state.
// App.js
const preferColorSchemeQuery = "(prefers-color-scheme: dark)";
const giveInitialTheme = () =>
localStorage.getItem("theme") ||
(matchMedia(preferColorSchemeQuery).matches ? "dark" : "light");
const App = () => {
const [theme, setTheme] = useState(giveInitialTheme());
const toggleTheme = () =>
setTheme((theme) => (theme === "light" ? "dark" : "light"));
useEffect(() => {
localStorage.setItem("theme", theme);
}, [theme]);
return (
<input
type="checkbox"
name="theme-toggle"
id="theme-toggle"
checked={theme && theme === "dark"}
onChange={toggleTheme}
/>
);
}
Here, we also have to make sure to update the local storage value of the theme. We do this with the help of the useEffect
hook. useEffect
runsĀ after React renders the component and ensures that the effect callback does not block the browserās visual painting.
Avoiding the flicker
To avoid the famous flicker we need to perform the DOM updates before React renders the component & the browser paints the visual screen. But, as we have seen above useEffect
can only help us perform operations after the render has been committed to the screen. Hence, the flicker.
Let me introduce you to another hook, useLayoutEffect
. The syntax for this hook is identical to that of useEffect
. The callback passed to this hook runs synchronously immediately after React has performed all DOM mutations. The code runs immediately after the DOM has been updated, but before the browser has had a chance to paint those changes.
ā ļø Warning
Prefer the standardĀ useEffect
when possible to avoid blocking visual updates.
So, we will be performing our updates with the help of useLayoutEffect
.
What updates?
We will have to update our CSS to match the current theme. Seems like a big task, doesnāt it? There are many ways to update the CSS, but, we will go forward with the most efficient way, i.e. CSS Variables or Custom Properties.
CSS variables are entities defined by CSS authors that contain specific values to be reused throughout a document. They are set using custom property notation (e.g.,Ā --main-color: black;
) and are accessed using theĀ var()
function (e.g.,Ā color: var(--main-color);
).
We can also use HTML data-*
attributes with CSS to match the data attribute & apply styles accordingly. In our case, depending on the data-theme
attribute value, different colors will be applied to our page.
/* index.css */
[data-theme="light"] {
--color-foreground-accent: #111111;
--color-foreground: #000000;
--color-background: #ffffff;
}
[data-theme="dark"] {
--color-foreground-accent: #eeeeee;
--color-foreground: #ffffff;
--color-background: #000000;
}
.app {
background: var(--color-background);
color: var(--color-foreground);
}
Our application code now will look something like this:
// App.js
const preferColorSchemeQuery = "(prefers-color-scheme: dark)";
const giveInitialTheme = () =>
localStorage.getItem("theme") ||
(matchMedia(preferColorSchemeQuery).matches ? "dark" : "light");
const App = () => {
const [theme, setTheme] = useState(giveInitialTheme());
const toggleTheme = () =>
setTheme((theme) => (theme === "light" ? "dark" : "light"));
useEffect(() => {
localStorage.setItem("theme", theme);
}, [theme]);
useLayoutEffect(() => {
if (theme === "light") {
document.documentElement.setAttribute("data-theme", "light");
} else {
document.documentElement.setAttribute("data-theme", "dark");
}
}, [theme]);
return (
<input
type="checkbox"
name="theme-toggle"
id="theme-toggle"
checked={theme && theme === "dark"}
onChange={toggleTheme}
/>
);
}
Access to the theme
The theme value might be needed anywhere across the application. We have to take care of this too. For this purpose, we store our theme value in a context & wrap its provider around the App
component.
// theme-context.js
// create theme context
const ThemeContext = createContext();
const preferColorSchemeQuery = "(prefers-color-scheme: dark)";
const giveInitialTheme = () =>
localStorage.getItem("theme") ||
(matchMedia(preferColorSchemeQuery).matches ? "dark" : "light");
// theme context provider
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState(giveInitialTheme());
const toggleTheme = () =>
setTheme((theme) => (theme === "light" ? "dark" : "light"));
useEffect(() => {
localStorage.setItem("theme", theme);
}, [theme]);
useLayoutEffect(() => {
if (theme === "light") {
document.documentElement.setAttribute("data-theme", "light");
} else {
document.documentElement.setAttribute("data-theme", "dark");
}
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// custom hook to avail theme value
const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};
// exports
export { ThemeProvider, useTheme };
Congratulations! We are done with the implementation. You now know how to implement Dark Mode in your React application. Go and implement this super cool feature in your application now. š„³
Extra feature
Consider a case, where the user changes the system-wide theme preference while he/she is using your application. In the implementation above, the application wonāt be able to detect these changes. If you want your application to detect these changes, we will need to set up a change
event listener on this system-wide theme preference. We can do this with the help of the useEffect
hook.
useEffect(() => {
const mediaQuery = matchMedia(preferColorSchemeQuery);
const handleColorSchemeChange = () =>
setTheme(mediaQuery.matches ? "dark" : "light");
mediaQuery.addEventListener("change", handleColorSchemeChange);
return () =>
mediaQuery.removeEventListener("change", handleColorSchemeChange);
}, []);
We add a change
event listener to the mediaQuery
on the mount. The final theme context will look something like this:
// theme-context.js
// create theme context
const ThemeContext = createContext();
const preferColorSchemeQuery = "(prefers-color-scheme: dark)";
const giveInitialTheme = () =>
localStorage.getItem("theme") ||
(matchMedia(preferColorSchemeQuery).matches ? "dark" : "light");
// theme context provider
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState(giveInitialTheme());
const toggleTheme = () =>
setTheme((theme) => (theme === "light" ? "dark" : "light"));
useEffect(() => {
const mediaQuery = matchMedia(preferColorSchemeQuery);
const handleColorSchemeChange = () =>
setTheme(mediaQuery.matches ? "dark" : "light");
mediaQuery.addEventListener("change", handleColorSchemeChange);
return () =>
mediaQuery.removeEventListener("change", handleColorSchemeChange);
}, [])
useEffect(() => {
localStorage.setItem("theme", theme);
}, [theme]);
useLayoutEffect(() => {
if (theme === "light") {
document.documentElement.setAttribute("data-theme", "light");
} else {
document.documentElement.setAttribute("data-theme", "dark");
}
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// custom hook to avail theme value
const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};
// exports
export { ThemeProvider, useTheme };
You can refer to the Codesandbox below:
Please feel free to share your feedback in the comments section. You can connect with me on Twitter or LinkedIn.
Happy hacking! Keep learning! š
Top comments (2)
i had no idea about implementing dark mode. really thanks for this detailed explanation loved it.
very detailed explanation, good work!