How to implement Dark and Light Mode Themes in Next.js application.
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:
- Basic knowledge of React and Next.js.
- 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
>_Terminalnpx 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
>_Terminalnpm 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.tsximport "./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.