Light, Dark and System Modes in Next.js with TailwindCSS
The complete pattern for dark mode is to allow users to choose between light, dark and system modes. System mode means that the website will automatically switch between light and dark mode based on the system settings. This article will show you how to implement this pattern in Next.js with TailwindCSS.
Set Up Next.js with TailwindCSS
First, we need to set up Next.js with TailwindCSS. You can follow the official guide here.
Note when you have to choose "Would you like to use Tailwind CSS? No / Yes" in the guide, choose "Yes".
Then you will get the document structure like this (App Route):
1.2├── app3│ ├── layout.js4│ ├── page.js5│ └── global.css
Integrate next-themes
with TailwindCSS
next-themes
with TailwindCSSInstall next-themes
next-themes
We need to install next-themes
to help us switch between light, dark and system modes. You can install it by running the following command:
1npm install next-themes
Chanage darkMode to class
class
Go to tailwind.config.js
and change darkMode to class
:
1module.exports = {2 darkMode: "class",3};
This will allow us to use the dark
class to switch between light and dark mode. Specifically, when <html>
tag has the dark
class, the website will be in dark mode and you can style it with the dark:
prefix by TailwindCSS.
Wrap the App with ThemeProvider
ThemeProvider
Create a file called provider.tsx
in the app
folder and add the following code:
1"use client";23import { ThemeProvider } from "next-themes";45export function Providers({ children }: { children: React.ReactNode }) {6 return <ThemeProvider attribute="class">{children}</ThemeProvider>;7}
Note you should let attribute
be class
so that we can use the dark
class to switch between light and dark mode.
Then, wrap the App with Providers
in app/layout.tsx
:
1import { Providers } from "./providers";23function RootLayout({ children }: { children: React.ReactNode }) {4 return (5 <html lang="en" suppressHydrationWarning>6 <body className="min-h-screen dark:bg-slate-900">7 <Providers>8 <main className="px-6 md:px-16 max-w-5xl mx-auto">{children}</main>9 </Providers>10 </body>11 </html>12 );13}
Add a Button to Switch Between Light, Dark and System Modes
Create components folder inside the app
folder and create a file called ThemeMenu.tsx
inside the components
folder.
Use setTheme()
from next-themes
to switch between light, dark and system modes.
1const { theme, setTheme } = useTheme();
Then, add a button to switch between light, dark and system modes, for example:
1import { useTheme } from "next-themes";23const ThemeChanger = () => {4 const { theme, setTheme } = useTheme();56 return (7 <div>8 The current theme is: {theme}9 <button onClick={() => setTheme("light")}>Light Mode</button>10 <button onClick={() => setTheme("dark")}>Dark Mode</button>11 </div>12 );13};
The above code is hydration unsafe and will throw a hydration mismatch warning when rendering with SSG or SSR. This is because we cannot know the theme on the server, so it will always be undefined
until mounted on the client.
To fix this, check the document of next-themes
here.
I created the theme switch menu with Headless UI and TailwindCSS. Here is the code:
1import { Menu, Transition } from "@headlessui/react";2import { Fragment, useEffect, useState } from "react";3import { SunIcon, MoonIcon, CogIcon } from "@heroicons/react/24/solid";4import { useTheme } from "next-themes";5import { ReactElement } from "react";67export default function ThemeMenu() {8 const [mounted, setMounted] = useState(false);9 const { theme, setTheme } = useTheme();1011 useEffect(() => {12 setMounted(true);13 }, []);1415 return (16 <Menu as="div" className="relative inline-block text-left">17 <div className="flex items-center justify-center w-8 h-8 rounded-md hover:bg-gray-100 dark:hover:bg-slate-800">18 <Menu.Button className="w-6 h-6">19 {mounted ? (20 <div>21 <SunIcon id="header__light" />22 <MoonIcon id="header__dark" className="p-0.5" />23 </div>24 ) : (25 // when unmounted, render an empty div to occupy the space26 <div />27 )}28 </Menu.Button>29 </div>30 <Transition31 as={Fragment}32 enter="transition ease-out duration-100"33 enterFrom="transform opacity-0 scale-95"34 enterTo="transform opacity-100 scale-100"35 leave="transition ease-in duration-75"36 leaveFrom="transform opacity-100 scale-100"37 leaveTo="transform opacity-0 scale-95"38 >39 <Menu.Items className="absolute right-0 mt-2 w-28 origin-top-right rounded-md bg-white dark:bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none z-50">40 <div className="px-1 py-1 ">41 <ThemeMenuItem42 theme="light"43 icon={<SunIcon className="h-5 w-5" />}44 label="Light"45 setTheme={setTheme}46 selected={theme === "light"}47 />48 <ThemeMenuItem49 theme="dark"50 icon={<MoonIcon className="h-4 w-4" />}51 label="Dark"52 setTheme={setTheme}53 selected={theme === "dark"}54 />55 <ThemeMenuItem56 theme="system"57 icon={<CogIcon className="h-5 w-5" />}58 label="System"59 setTheme={setTheme}60 selected={theme === "system"}61 />62 </div>63 </Menu.Items>64 </Transition>65 </Menu>66 );67}6869function ThemeMenuItem({70 theme,71 icon,72 label,73 setTheme,74 selected,75}: {76 theme: string;77 icon: ReactElement;78 label: string;79 setTheme: (theme: string) => void;80 selected: boolean;81}) {82 return (83 <Menu.Item>84 {({ active }) => (85 <button86 className={generateClassName(active, selected)}87 onClick={() => setTheme(theme)}88 >89 <div className="flex items-center justify-center h-5 w-5 mr-2">90 {icon}91 </div>92 {label}93 </button>94 )}95 </Menu.Item>96 );97}9899function generateClassName(active: boolean, selected: boolean) {100 let baseClass =101 "group flex w-full items-center rounded-md px-2 py-1.5 my-0.5";102 if (active) {103 baseClass = `${baseClass} bg-gray-100 dark:bg-slate-600`;104 }105 if (selected) {106 baseClass = `${baseClass} ring-1 ring-orange-500 ring-opacity-60`;107 }108 return baseClass;109}
With the CSS code in global.css
:
1#header__light,2#header__dark {3 display: none;4}56html[class="dark"] #header__dark {7 display: block;8}910html[class="light"] #header__light {11 display: block;12}
This piece of code is used to show the corresponding icon according to the current theme. When user choose the system mode, the icon will be his system theme on his device, which is determined by prefers-color-scheme
.
Now you can switch between light, dark and system modes by this menu button. You only need to add dark:
prefix to the class name to make the dark mode style work.