How to implement Dark and Light Mode Themes in Next.js application.

How to implement Dark and Light Mode Themes in Next.js application.

authorBy Kaimul
6 min read

Creating a dynamic theme system in Next.js that allows users to switch between dark and light modes...


Introduction: Creating a dynamic theme system in Next.js that allows users to switch between dark and light modes enhances the user’s experiences by providing visual comfort and personalization . This blog will guide you through the process off implementing a themes switcher in the next.js application using css, tailwind and next-theme library.

Prerequisites:

Before we dive into the implementation, make sure you have the following prerequisites:

  1. Basic knowledge of React and Next.js.
  2. A Next.js project is set up and running.

Step: 1 Setting up the project

First , ensure that you have a next.js project set up . if you don’t have one can create new project by running

>_Terminal
npx create-next-app@latest my-app` cd my-app

Step: 2 Install next-themes library

Install next-themes using using your preferred package manager by running

>_Terminal
npm install next-themes

Step: 3. Set up ThemeProvider

Now, we need to wrap our root layout with the ThemeProvider component, which is imported from next-themes. Since our root layout is a server component, whereas ThemeProvider is a client component, we cannot directly wrap a server component with a client component. To achieve this, we’ll create a client component called components/Provider.tsx, where we’ll import the ThemeProvider component and then wrap the root layout with it. Take a look at the following code snippet for better understanding.

components/Provider.tsx
"use client"; import { ThemeProvider } from "next-themes"; import React, { ReactNode } from "react"; interface ProviderProps { children: ReactNode; } export default function Provider({ children }: ProviderProps) { return <ThemeProvider disableTransitionOnChange>{children}</ThemeProvider>; }

Now, we will import this Provider.tsx in rootLayout

app/layout.tsx
import "./globals.scss"; import type { Metadata } from "next"; import { Montserrat } from "next/font/google"; import Providers from "./providers"; const montserrat = Montserrat({ subsets: ["latin"], display: "swap", }); export const metadata: Metadata = { title: "Next themes", description: "Creating a dynamic theme system in Next.js", }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en" className={montserrat.className} suppressHydrationWarning> <body> <Providers>{children}</Providers> </body> </html> ); }

Step: 4 Set up style

in the global.css, Please place the code below

scss
:root { /* Your default theme */ --background: white; --foreground: black; } [data-theme="dark"] { --background: black; --foreground: white; }

Step: 5 Set up ThemToggleButton

Now, we'll create a ThemeToggleButton component that we can place in the navbar to switch between dark and light mode. The following code snippet will get the job done. Before copying the code, let me explain how this code is going to work.

We use the useTheme hook from "next-themes". Our UI needs to know the current theme to be able to change it. The useTheme hook will provide the theme information. If you comment out useState and useEffect then click the ThemeToggleButton, you will see a hydration mismatch warning. This occurs when we're rendering our UI with SSR (Server-side rendering) or SSG (Static Site Generation).

We cannot know the theme on the server, as many of the values returned from useTheme will be undefined until mounted on the client. This means if you try to render UI based on the current theme before mounting on the client, you will see a hydration mismatch error. To avoid this hydration mismatch error, we need to make sure that we only render UI that uses the current theme when the page is mounted on the client. So, we will conditionally render our UI.

With the help of code inside the useEffect, in the navbar We will see the ThemeToggleButton when mounted is true; otherwise, return null.

In our code, instead of returning null, we use a button skeleton to avoid layout shift. Now we see the ThemeToggleButton when mounted; if not, we will see the skeleton.

tsx
"use client"; import styles from "./themeButton.module.scss"; import { useTheme } from "next-themes"; import { useEffect, useState } from "react"; import { LuSun } from "react-icons/lu"; import { RxMoon } from "react-icons/rx"; import ThemeSkeleton from "../Skeleton/Skeleton"; export default function ThemeButton() { const { resolvedTheme, setTheme } = useTheme(); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); if (!mounted) { return <ThemeSkeleton />; } return ( <button className={styles.btn} type="button" onClick={()=> setTheme(resolvedTheme= "dark" ? "light" : "dark")} title="Dark & Light" > {resolvedTheme === "dark" ? ( <LuSun size="3rem" /> ) : ( <RxMoon size="3rem" /> )} </button> ); }

Why do we use resolvedTheme instead of theme?

When we support the System theme preference (which can be light or dark), we want our UI to reflect that choice accurately. For example, if the system is set to “System” theme, our UI should show “System” instead of just “Light” or “Dark”.

Without resolvedTheme:

  • Our UI might incorrectly show “Light” or “Dark” when it should show “System”.

With resolvedTheme:

  • We can determine what the “System” theme actually resolves to (either “Light” or “Dark”) and adjust our UI accordingly.

Example:

const { resolvedTheme, setTheme } = useTheme();

onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}

This way, we know if the system theme is dark or light and can style our components correctly.

In short, resolvedTheme helps us know the actual theme being used, even when the system preference is set to “System”. This ensures our UI is always accurate and consistent.

Styling with Tailwind css

In our tailwind.config.js, we set the dark mode property to class:

css
// tailwind.config.js module.exports = { darkmode: "class"; }

We have to set the attribute for Theme Provider to class:

jsx
<html lang="en" className={montserrat.className} suppressHydrationWarning> <body> <Providers attribute="class">{children}</Providers> </body> </html>

That's it! Now we can use dark-mode specific classes: for example

jsx
<div className=”bg-red-300 dark:bg-teal-300”/> <h1 className="text-black dark:text-white">

Styling with SCSS

By default, dark mode and light mode work when you click ThemeToggleButton based on the background and foreground colors we set in global.css.

However, we can customize our app in any way we prefer. For example

scss
.testimonials { width: 100%; min-height: 500px; .container { background-color: #f5eeee; .left { background-color: $common-color; h1{ color: $dark } .right { .slide { background-color: $white-color; } } } } [data-theme="dark"] { .testimonials { .container { background-color: $light-dark; .left { background-color: $dark; h1{ color: $white-color } .right { .slide { background-color: $dark; } } } } }

Conclusion

Finally, we've completed implementing a dynamic theme system in a Next.js application using the next-themes library. This implementation provides a smooth user experience while switching between dark and light modes. We have successfully handled hydration errors and used skeletons to prevent layout shifts.