Enabling dark mode with Tailwind couldn't be easier, just use the dark variant e.g.:
<h1className="text-zinc-700 dark:text-zinc-300">Hello dark mode</h1>
In the snippet above the text-zinc-700 is used in light mode and
text-zinc-300 is used if dark mode is enabled on your operating system.
This works because Tailwind uses prefers-color-scheme media query.
But if we want to allow the user to switch between light and dark mode,
things are getting complicated.
By setting darkMode to class Tailwind applies the dark mode variant if a parent element has the class dark.
Now we can implement a simple dark mode toggle:
This example renders a moon icon (from the lucide-react package).
After a click on the icon, the dark class is toggled on the html element and the icon switches to a sun.
If the dark class is applied to the html element, we should see that the dark mode is applied to our page.
But if we refresh the page, we are back on light mode.
So we need a way to persist our selection.
According to the Tailwind docs a good way to store the selected mode is the localStorage.
So we could extend our toggle button and store the selection in the localStorage:
Now we are able to apply the selected mode on page load.
We should do this as early as possible during the rendering of our page to avoid flickering:
app/layout.tsx
constRootLayout:FC<PropsWithChildren> = ({ children }) => (
<htmllang="en">
<head>
<script
dangerouslySetInnerHTML={{
__html:`
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
`,
}}
/>
</head>
<body>{children}</body>
</html>
);
exportdefault RootLayout;
The snippet above adds the dark class,
if the theme key in the localStorage is set to dark or
if the key is not set and the prefers-color-scheme matches dark.
Otherwise the class is removed from the html element.
Finally, we should reflect the current mode in our toggle button.
components/DarkModeToggle.tsx
"use client";
import { Moon, Sun } from"lucide-react";
import { useEffect, useState } from"react";
constDarkModeToggle= () => {
const [mode,setMode] =useState("light");
useEffect(() => {
if (document.documentElement.classList.contains("dark")) {
react_devtools_backend.js:4026 Warning: Prop `className` did not match. Server: "dark" Client: ""
at html
at ReactDevOverlay (webpack-internal:///./node_modules/.pnpm/next@13.0.3_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:53:9)
at HotReload (webpack-internal:///./node_modules/.pnpm/next@13.0.3_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:19:11)
at Router (webpack-internal:///./node_modules/.pnpm/next@13.0.3_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/app-router.js:74:11)
at ErrorBoundaryHandler (webpack-internal:///./node_modules/.pnpm/next@13.0.3_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/error-boundary.js:28:9)
at ErrorBoundary (webpack-internal:///./node_modules/.pnpm/next@13.0.3_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/error-boundary.js:40:11)
at AppRouter
at ServerRoot (webpack-internal:///./node_modules/.pnpm/next@13.0.3_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/app-index.js:113:11)
at RSCComponent
at Root (webpack-internal:///./node_modules/.pnpm/next@13.0.3_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/app-index.js:130:11)
We see this error,
because of the fact that the server renders the html element without the dark class,
even if dark mode is enabled.
But we add the class before react hydrates the document.
I don't know how to avoid this error,
but the good news is that the error only shows up in development mode.
The error can be suppressed by using the suppressHydrationWarning property on the html element:
app/layout.tsx
<htmllang="en"suppressHydrationWarning={true}>
This will suppress the warning only for the html element, not for its children.