How to Implement Dark Mode in React/Next.js Using Sass

Dark mode has become a popular feature on websites, and we'll explore a simple yet scalable solution for your React/Next.js app using Sass.

ยท12 minutes reading
Cover Image for How to Implement Dark Mode in React/Next.js Using Sass

In this post, we'll learn how to toggle between light and dark mode in React and Sass. We will start with a single simple button on the main page and use Sass to style it.

Later, however, we will create a more scalable solution using React Context to manage the state of the theme so that all components in the app can access it.

๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Here's What You'll Learn

  • Implement a dark mode toggle
  • Create a Next.js 13 app with the latest features (app router)
  • Implement and use Sass
  • Use custom hooks and React Context
  • Apply the theme anywhere in your app

๐Ÿ“บ Video Tutorial

If you prefer to watch a video tutorial instead, here you go! ๐Ÿ‘‡
If not, you can skip this section and continue reading the article below.

Simple Toggle Button on a Single Page

Let's start with a simple toggle button on a single page. We will use React hooks to manage the state of the theme and Sass to style the button.

Set up Next.js and Sass

Start by setting up Next.js and Sass. We will use the latest version of Next.js with the App Router.

Install Next.js
npx create-next-app@latest

On installation, you'll see the following prompts:

Next.js prompts
What is your project named? theme-toggle
Would you like to use TypeScript with this project? No / Yes
Would you like to use ESLint with this project? No / Yes
Would you like to use Tailwind CSS with this project? No / Yes
Would you like to use `src/` directory with this project? No / Yes
Use App Router (recommended)? No / Yes
Would you like to customize the default import alias? No / Yes

Select an appropriate name and select 'No' for TypeScript, Tailwind CSS, src/ directory, and import alias. Select 'Yes' for App Router, and ESLint.

Next, cd to your project and install Sass.

Install Sass
npm install --save-dev sass

We will also an icon library called Heroicons. Install it as well, but you can use any icon library you want.

Install Heroicons
npm install @heroicons/react

Then change app/globals.css to app/globals.scss and modify app/layout.js as well.

app/layout.js
import './globals.scss';
import { Inter } from 'next/font/google';
 
const inter = Inter({ subsets: ['latin'] });
 
export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
};
 
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  );
}

Create a Simple Button

Now let's create a simple button in app/page.js. We use useEffect to toggle the mode in the body class.

app/page.js
'use client';
 
import { useState } from 'react';
import { SunIcon, MoonIcon } from '@heroicons/react/24/outline';
 
export default function Home() {
  const [isDarkMode, setIsDarkMode] = useState(false);
 
  const toggleTheme = () => {
    setIsDarkMode(!isDarkMode);
    document.documentElement.classList.toggle('dark');
  };
 
  return (
    <main className="container">
      <div className="content">
        <h1 className="title">Theme Toggle in React/Next.js with Sass</h1>
 
        <button
          aria-label="Toggle Dark Mode"
          className="toggle-button"
          onClick={toggleTheme}
        >
          {isDarkMode ? (
            <MoonIcon className="icon" />
          ) : (
            <SunIcon className="icon" />
          )}
        </button>
      </div>
    </main>
  );
}
app/page.js
'use client';
 
import { useState } from 'react';
import { SunIcon, MoonIcon } from '@heroicons/react/24/outline';
 
export default function Home() {
  const [isDarkMode, setIsDarkMode] = useState(false);
 
  const toggleTheme = () => {
    setIsDarkMode(!isDarkMode);
    document.documentElement.classList.toggle('dark');
  };
 
  return (
    { /* Step 1 */}
    <main className="container">
 
    </main>
 
    { /* Step 2 */}
    <main className="container">
      <div className="content">
 
      </div>
    </main>
 
    { /* Step 3 */}
     <main className="container">
      <div className="content">
        <h1 className="title">Theme Toggle in React/Next.js with Sass</h1>
 
      </div>
    </main>
 
    { /* Step 4 */}
     <main className="container">
      <div className="content">
        <h1 className="title">Theme Toggle in React/Next.js with Sass</h1>
 
        <button
          aria-label="Toggle Dark Mode"
          className="toggle-button"
          onClick={toggleTheme}
        >
          {isDarkMode ? (
            <MoonIcon className="icon" />
          ) : (
            <SunIcon className="icon" />
          )}
        </button>
      </div>
    </main>
  );
}

This React component renders a button to toggle between dark and light themes. We use the useState hook and our toggleTheme function to handle the theme state and apply the appropriate CSS class to the document root element.

The button's appearance and text change based on the theme state.

We need to use 'use client' to tell the compiler that we want to use the client-side version of React.

This is because we want to use React hooks. At default, the compiler will use the server-side version of React and hooks are not supported there.

Add Some Styles

Let's add some styles to the button. We first create some variables for the colors and then create .toggle-button class.

app/globals.scss
$slate-50: #f8fafc;
$slate-100: #f1f5f9;
$slate-200: #e2e8f0;
$slate-300: #cbd5e1;
$slate-400: #94a3b8;
$slate-500: #64748b;
$slate-600: #475569;
$slate-700: #334155;
$slate-800: #1e293b;
$slate-900: #0f172a;
$slate-950: #020617;
 
$purple-50: #faf5ff;
$purple-100: #f3e8ff;
$purple-200: #e9d5ff;
$purple-300: #d8b4fe;
$purple-400: #c084fc;
$purple-500: #a855f7;
$purple-600: #9333ea;
$purple-700: #7e22ce;
$purple-800: #6b21a8;
$purple-900: #581c87;
$purple-950: #3b0764;
 
:root {
  background-color: white;
  color: $slate-950;
 
  &.dark {
    background-color: $slate-950;
    color: $slate-100;
  }
}
 
.toggle-button {
  border-radius: 999px;
  background-color: $purple-500;
  padding: 0.75rem;
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: auto;
  margin: auto;
  cursor: pointer;
 
  &:hover {
    background-color: $purple-400;
  }
 
  .icon {
    height: 1.75rem;
    width: 1.75rem;
    color: white;
  }
}
 
// Basic styling for the page
 
.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
}
 
.content {
  margin: auto;
  max-width: 80rem;
  text-align: center;
}
 
.title {
  margin: 1.5rem 0;
  font-weight: 600;
}

More Scalable Solution With React Context

To ensure that the theme change affects all components the app, we use React Context.

This will allow us to share the theme state across all components and update them accordingly when the theme is toggled.

Create a Context and Provider

First, we need to create a context and a provider. We create one in the root folder context/ThemeContext.js.

context/ThemeContext.js
'use client';
 
import { createContext, useContext, useState } from 'react';
 
export const ThemeContext = createContext();
 
export const ThemeProvider = ({ children }) => {
  const [isDarkMode, setIsDarkMode] = useState(false);
 
  const toggleTheme = () => {
    setIsDarkMode(!isDarkMode);
    document.documentElement.classList.toggle('dark');
  };
 
  return (
    <ThemeContext.Provider value={{ isDarkMode, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};
 
export const useTheme = () => useContext(ThemeContext);

Wrap the App With the Provider

Then we wrap the ThemeProvider around the RootLayout in app/layout.js.

app/layout.js
import './globals.scss';
import { Inter } from 'next/font/google';
import { ThemeProvider } from '@/context/ThemeContext';
 
const inter = Inter({ subsets: ['latin'] });
 
export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
};
 
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

Use the Context for the Toggle Button

Now we can use the useTheme hook to access the theme state and toggle function. Let's do this for the toggle button in app/page.js.

app/page.js
'use client';
 
import { useTheme } from '@/context/ThemeContext';
import { SunIcon, MoonIcon } from '@heroicons/react/24/outline';
 
export default function Home() {
  const { isDarkMode, toggleTheme } = useTheme();
 
  return (
    <main className="container">
      <div className="content">
        <h1 className="title">Theme Toggle in React/Next.js with Sass</h1>
 
        <button
          aria-label="Toggle Dark Mode"
          className={`toggle-button ${isDarkMode ? 'dark' : 'light'}`}
          onClick={toggleTheme}
        >
          {isDarkMode ? (
            <MoonIcon className="icon" />
          ) : (
            <SunIcon className="icon" />
          )}
        </button>
      </div>
    </main>
  );
}

Create a Card Component

Now, every other component can access the theme state and react accordingly when the theme is toggled. Let's try this with a simple card component.

Let's first create a 'components' folder and create a Card.js component.

components/Card.js
import { useTheme } from '@/context/ThemeContext';
 
const Card = () => {
  const { isDarkMode } = useTheme();
 
  return (
    <div className={`card ${isDarkMode ? 'dark' : 'light'}`}>
      This card is on {isDarkMode ? 'dark mode' : 'light mode'}.
    </div>
  );
};
 
export default Card;

Move the Toggle Button to Its Own Component

Let's also move the toggle button to its own component in components/ThemeToggleBtn.js.

components/ThemeToggleBtn.js
import { SunIcon, MoonIcon } from '@heroicons/react/24/outline';
import { useTheme } from '@/context/ThemeContext';
 
const ThemeToggleBtn = () => {
  const { isDarkMode, toggleTheme } = useTheme();
 
  return (
    <button
      aria-label="Toggle Dark Mode"
      className="toggle-button"
      onClick={toggleTheme}
    >
      {isDarkMode ? (
        <MoonIcon className="icon" />
      ) : (
        <SunIcon className="icon" />
      )}
    </button>
  );
};
 
export default ThemeToggleBtn;

Move the Card and Toggle Button to the Page

Then we import the Card and ThemeToggleBtn component in app/page.js.

app/page.js
'use client';
 
import Card from '@/components/Card';
import ThemeToggleBtn from '@/components/ThemeToggleBtn';
 
export default function Home() {
  return (
    <main className="container">
      <div className="content">
        <h1 className="title">Theme Toggle in React/Next.js with Sass</h1>
 
        <ThemeToggleBtn />
 
        <Card />
      </div>
    </main>
  );
}

Add Styling for the Card Component

Lastly, we need to add some styling for the card component in globals.scss.

app/globals.scss
.card {
  overflow: hidden;
  border-radius: 0.5rem;
  padding: 1.5rem;
  margin: 1.5rem 0;
 
  &.light {
    background-color: $slate-600;
    color: $slate-200;
  }
 
  &.dark {
    background-color: $slate-200;
    color: $slate-950;
  }
}
 
// Rest of the code

Now, when we toggle the theme, the card component will also change its theme accordingly.

This is because the card component is using the useTheme hook to access the theme state and react accordingly when the theme is toggled.

Conclusion

And that's how we can create a simple toggle button in React/Next.js with Sass. We also learned how to make the theme change affect all components in the app using React Context.

๐Ÿ“š Materials/References