How to implement Dark mode with Next.js and CSS variables
Next.js

How to implement Dark mode with Next.js and CSS variables

Having a dark mode is a common feature for modern websites. It would be great for user experience to have an option to toggle between light / dark mode depending on user's preference.

In this article, I'll show you how to implement a dark mode in your Next.js apps.

Prerequisite

What we build

These are the features of what we are gonna build.

  • The user can toggle to switch between Light and Dark mode.
  • The user's system settings will be applied for their first visit.
  • The prefered theme setting will be preserved in localStorage for their next visit.

If this is what you want, follow along this tutorial.

Step 1. Install next-themes package

We need only 1 package for implementing Dark mode.

Open the terminal and install next-themes package.

console
$ npm install next-themes
# or
$ yarn add next-themes

Step 2. Create a CSS file for managing colors

Now, we need to create a CSS file to store all the colors in Light / Dark mode. Create /styles/colors.css file and set your colors like this:

/styles/colors.css
CSS
:root {
  /* Your default theme */
  --background: white;
  --foreground: black;
}

[data-theme="dark"] {
  --background: black;
  --foreground: white;
}

The root pseudo-class selects the root element which is html tag. The variables in this scope will be applied to Light mode.

And as you can guess, [data-theme='dark'] will be applied to Dark mode. This data-theme attribute is added and updated whenever the user toggles the theme.

Step 3. Use CSS variables in your .css files

Skip this section if you already know how to use CSS variables.

Previously we created a colors.css and configured colors for Light / Dark mode. Now, we need to apply those CSS variables in the stylesheets.

Here's an exapmle of how you write CSS variables.

/styles/globals.css
CSS
body {
  background-color: var(--background);
  color: var(--foreground);
}

Like the example code above, replace all the hard-coded colors with CSS variables.

Step 4. add ThemeProvider in _app.js

To enable dark mode, we need to do 2 things in _app.js file which are the following:

  1. Wrap the whole app with ThemeProvider
  2. Import colors.css
_app.js
JavaScript
import "@/styles/colors.css";
import { ThemeProvider } from "next-themes";

export default function App({ Component, pageProps }) {
  return (
    <ThemeProvider>
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

That's it! Now your Next.js app supports dark mode.

If you refresh the page, you'll see data-theme attribute on html tag. The value would be either light or dark depending on your system settings.

Step 5. create Theme Switch component

Now that we've implemented a dark mode, let's create a component to toggle between light and dark mode.

Create /components/theme-switch.jsx file and paste the code below:

/components/theme-switch.jsx
JSX
import { useState, useEffect } from "react";
import { useTheme } from "next-themes";

export default function ThemeSwitch() {
  const [mounted, setMounted] = useState(false);
  const { resolvedTheme, setTheme } = useTheme();

  // useEffect only runs on the client, so now we can safely show the UI
  useEffect(() => {
    setMounted(true);
  }, []);
  if (!mounted) {
    return null;
  }

  return (
    <>
      <p>Current theme: {resolvedTheme}</p>
      <button
        onClick={() => setTheme(resolvedTheme === "light" ? "dark" : "light")}
      >
        <span>Toggle Theme</span>
      </button>
    </>
  );
}

I prepared the live demo of this code. You can try it out by clicking the "Toggle Theme" button.

Every time you click on the button, the value of data-theme attribute on html element will be switched.

Code explanation

First of all, we need to write a code to avoid hydration mismatch error.

JSX
useEffect(() => {
  setMounted(true);
}, []);
if (!mounted) {
  return null;
}

return {
  /* component here */
};

This is officially recommended way so if you wanna know more about it, please check out the next-themes GitHub page.

setTheme

To set the theme, we use setTheme function prepared by next-themes.

JSX
// Set [data-theme="light"]
<button onClick={() => setTheme("light")}>

You can simply pass the theme name as a string, and it will set it to data-theme attribute of the root element.

Since we wanna toggle the themes, we can write like this:

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

If the current theme is light, switch it to dark, otherwise set is as light.

resolvedTheme

next-themes provides theme and resolvedTheme that return the active theme name.

These two behave almost the same but the difference is whether it supports system theme or not.

JavaScript
console.log("theme: ", theme);
console.log("resolvedTheme:", resolvedTheme);
The result of console.log

I changed my Mac OS setting to 'dark' and this is the result:

theme understands that the current theme is system, but it doesn't know whether the system setting is light or dark.

So if you want to support the system setting, use resolvedTheme.

Testing

Now, let's see if the code is working. When you visit the site for the first time, next-themes stores the active theme value in localStorage.

To see this, open your developer tool in your browser (In my case, I use Google Chrome). Click on [Application] tab and choose [Local Storage] from the left menu bar.

Local storage

For the first visitor, theme:system is stored in here. Try checking if the system setting is reflected by switching the appearance mode.

Whene system setting is light

System Settings > Appearance: Light

Whene system setting is dark

System Settings > Appearance: Dark

If the visitor clicks on the toggle button, theme value will be updated and stored in the storage.

You can check it yourself by clicking the button below:

That's it! Now your website supports the system theme, and is capable of switching between Light / Dark mode.

In this article, I showed you the most basic example. If you wanna know more about next-themes, please check out their official page.