Dark / Light Toggle Mode in NextJS

I am a software developer, passionate about developing and designing solutions with an emphasis on great user experience.
Implementing a dark/light toggle mode in web applications greatly enhances user experience by respecting users' preferences and comfort. In this article, we’ll explore how to add this feature to a NextJS web application and create an effect like the one shown below.

Setting up a NextJS Application
The first step is to create a NextJS app using the following command.
npx create-next-app@latest
Follow the steps outlined in the official docs.
Install the Next-Themes Library
We’ll use the next-themes library to manage the dark / light toggle mode in our application. The library is specifically designed to handle theme toggling with ease, making it a perfect choice for our needs.
npm i next-themes
Create Theme Toggle Provider component
We’ll now create a ThemeToggleProvider component, which will receive children and any additional props to be passed to NextThemesProvider.
Create a utils folder, and inside it, create a ThemeToggleProvider.tsx file. In the ThemeToggleProvider.tsx file, write the following code.
"use client";
import React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import useMounted from "@/hooks/useMounted";
function ThemeToggleProvider({children, ...props}: React.ComponentPropsWithoutRef<typeof NextThemesProvider>) {
const mounted = useMounted();
// if component is not mounted, return null
if (!mounted) return null;
// else render the NextThemesProvider component with received props and children
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
export default ThemeToggleProvider;
As shown above, we use the useMounted hook to prevent hydration errors that can occur when switching between dark and light themes. This hook returns a boolean value indicating whether the component has finished mounting (i.e., rendering) on the screen. The code for the hook is shown below.
"use client";
import { useState, useEffect } from "react";
function useMounted() {
const [mounted, setMounted] = useState<boolean>();
useEffect(() => {
setMounted(true);
}, []);
return mounted;
}
export default useMounted;
Use the ThemeToggleProvider in the Application
It’s time to use the ThemeToggleProvider we created earlier. Inside the app/layout.tsx file, we’ll import the component and wrap the children with it. Our app/layout.tsx file should now look like this.
import type { Metadata } from "next";
import React, { Suspense } from "react";
import { Poppins, Nunito_Sans } from "next/font/google";
import clsx from "clsx";
import ThemeToggleProvider from "@/utils/ThemeToggleProvider";
import Spinner from "@/components/ui/Spinner";
import "./globals.css";
// Poppins Font
const mainFont = Poppins({
subsets: ["latin"],
display: "fallback",
weight: ["400", "500", "600", "700", "800", "900"],
variable: "--font-poppins",
});
// Nunito Sans Font
const monoFont = Nunito_Sans({
subsets: ["latin"],
display: "fallback",
weight: ["400", "500", "600", "700", "800", "900"],
variable: "--font-nunito-sans",
});
// Metadata
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${clsx(mainFont.variable, monoFont.variable)} `}>
{/* ThemeToggleProvider for light/dark theme */}
<ThemeToggleProvider
attribute="class"
defaultTheme="system"
enableSystem
>
<Suspense fallback={<Spinner />}>{children}</Suspense>
</ThemeToggleProvider>
</body>
</html>
);
}
At this point, we have set up our application to use both dark and light modes. Now, we'll create a toggle button to switch between these two modes.
Create a Toggle Button component

Let's create an animated toggle button, as shown above, to switch between the two modes. Create a ThemeSwitcher.tsx file with the following code. We will use the useTheme hook from next-themes to get the current theme and manage theme switching and animation style, and motion for the SVG animation.
"use client";
import React from "react";
import { useTheme } from "next-themes";
import { motion } from "motion/react";
// properties for animation of light / dark button
export const animationProperties = {
dark: {
circle: {
r: 9,
},
mask: {
cx: "50%",
cy: "23%",
},
svg: {
transform: "rotate(40deg)",
},
lines: {
opacity: 0,
},
},
light: {
circle: {
r: 5,
},
mask: {
cx: "100%",
cy: "0%",
},
svg: {
transform: "rotate(90deg)",
},
lines: {
opacity: 1,
},
},
};
function ThemeSwitcher() {
const { theme, setTheme, systemTheme } = useTheme();
// get current theme
const currentTheme = theme === "system" ? systemTheme : theme;
// get animation state based on current theme
const animationState = animationProperties[currentTheme === "dark" ? "light" : "dark"];
return (
<button
onClick={() => setTheme(currentTheme === "dark" ? "light" : "dark")}
className="hover:text-white"
type="button"
aria-label="Toggle dark / light mode"
>
<motion.svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
stroke="currentColor"
animate={animationState.svg}
transition={{ type: "spring", stiffness: 250, damping: 35 }}
>
<mask id="logoMask">
<rect x="0" y="0" width="100%" height="100%" fill="white" />
<motion.circle
animate={animationState.mask}
r="7"
fill="black"
transition={{ type: "spring", stiffness: 250, damping: 35 }}
/>
</mask>
<motion.circle
cx="12"
cy="12"
animate={animationState.circle}
fill="currentColor"
stroke="none"
mask="url(#logoMask)"
transition={{ type: "spring", stiffness: 250, damping: 35 }}
/>
<motion.g
stroke="currentColor"
strokeWidth="2.5"
animate={animationState.lines}
transition={{ duration: 0.3 }}
>
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</motion.g>
</motion.svg>
</button>
);
}
export default ThemeSwitcher;
The application can now implement the ThemeSwitcher component, for example, in the header section. We’ll implement it before the children in the app/layout.tsx file.
import type { Metadata } from "next";
import React, { Suspense } from "react";
import { Poppins, Nunito_Sans } from "next/font/google";
import clsx from "clsx";
import ThemeToggleProvider from "@/utils/ThemeToggleProvider";
import ThemeSwitcher from "@/components/ThemeSwitcher";
import Spinner from "@/components/ui/Spinner";
import "./globals.css";
// Poppins Font
const mainFont = Poppins({
subsets: ["latin"],
display: "fallback",
weight: ["400", "500", "600", "700", "800", "900"],
variable: "--font-poppins",
});
// Nunito Sans Font
const monoFont = Nunito_Sans({
subsets: ["latin"],
display: "fallback",
weight: ["400", "500", "600", "700", "800", "900"],
variable: "--font-nunito-sans",
});
// Metadata
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${clsx(mainFont.variable, monoFont.variable)} `}>
{/* ThemeToggleProvider for light/dark theme */}
<ThemeToggleProvider
attribute="class"
defaultTheme="system"
enableSystem
>
<ThemeSwitcher />
<Suspense fallback={<Spinner />}>{children}</Suspense>
</ThemeToggleProvider>
</body>
</html>
);
}
Apply styling effects based on dark or light mode
We can now style the application based on its respective theme. We’ll use Tailwind CSS for styling.
In the tailwind.config.ts file, add darkMode: 'class'. This darkMode setting makes it easier to add styles specific to dark mode.
import type { Config } from "tailwindcss";
export default {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
darkMode: "class",
theme: {},
},
plugins: [],
} satisfies Config;
Now, we can add a dark: prefix before any Tailwind class to apply a style that will appear in dark mode. For example:
<h1 className="text-black dark:text-white"></h1>
Conclusion
In conclusion, using the next-themes library makes adding a dark/light toggle mode in a NextJS app much easier as it offers hooks and components for easy integration. The library works well with styling frameworks like Tailwind CSS, simplifying the application of theme-specific styles. This approach not only simplifies development but also enhances user experience by providing a dark mode option.



