I built my own Dark Mode with TailwindCSS (without using Next Themes)

I built my own Dark Mode with TailwindCSS (without using Next Themes)

In this article, I'll walk you through how I implemented a dark mode in my Next.js application using TailwindCSS. We'll be using the darkMode feature of TailwindCSS and the lucide-react library for icons. The entire process should take you about three minutes to read through.

Step 1: Install TailwindCSS and Lucide-React

First, make sure you have TailwindCSS and lucide-react installed in your project. You can install them using npm or yarn:

npm install tailwindcss lucide-react
# or
yarn add tailwindcss lucide-react

Step 2: Configure TailwindCSS for Dark Mode

In your TailwindCSS configuration file (tailwind.config.js), enable the darkMode option and set it to use the class strategy:

module.exports = {
  darkMode: ['class'],
  // other configurations...
};

This configuration means you will control dark mode by adding or removing the dark class on the html element.

Step 3: Create a Dark Mode Toggle Component (my method)

Next, create a DarkMode component that toggles the dark mode on and off. This component will receive dark and setDark props for managing the state.

"use client"

import { Moon, Sun } from "lucide-react";
import { useEffect } from "react";

export function DarkMode({ dark, setDark }) {
  useEffect(() => {
    if (typeof window !== "undefined") {
      const root = window.document.documentElement;
      if (dark) {
        root.classList.add('dark');
      } else {
        root.classList.remove('dark');
      }
    }
  }, [dark]);

  return (
    <div className="cursor-pointer" onClick={() => setDark(!dark)}>
      {dark ? <Sun className="h-[1.2rem] w-[1.2rem]" /> : <Moon className="h-[1.2rem] w-[1.2rem]" />}
    </div>
  );
}

Add a function to store the dark mode preference in local storage. This ensures that the user's preference is saved across sessions.

import { useState, useEffect } from 'react';

const useDarkMode = () => {
  const [dark, setDark] = useState(false);

  useEffect(() => {
    const storedDarkMode = localStorage.getItem('dark-mode');
    if (storedDarkMode) {
      setDark(storedDarkMode === 'true');
    }
  }, []);

  useEffect(() => {
    localStorage.setItem('dark-mode', dark);
  }, [dark]);

  return [dark, setDark];
};

export default useDarkMode;

Usage example:

import useDarkMode from './path/to/useDarkMode';
import { DarkMode } from './path/to/DarkMode';

export default function Header() {
  const [dark, setDark] = useDarkMode();

  return (
    <header className="p-4 flex justify-between items-center">
      <h1 className="text-2xl">My App</h1>
      <DarkMode dark={dark} setDark={setDark} />
    </header>
  );
}

Shorter version: DarkMode.tsx includes it all:

"use client"

import { Moon, Sun } from "lucide-react";
import { useEffect, useState } from "react";

export function DarkMode() {
  const [dark, setDark] = useState(false);

  useEffect(() => {
    // here we check the store for current status
    const storedDarkMode = localStorage.getItem('dark-mode');
    if (storedDarkMode) {
      setDark(storedDarkMode === 'true');
    }
  }, []);

  useEffect(() => {
    if (typeof window !== "undefined") {
      const root = window.document.documentElement;
      if (dark) {
        root.classList.add('dark');
      } else {
        root.classList.remove('dark');
      }
      localStorage.setItem('dark-mode', dark);
    }
  }, [dark]);

  return (
    <div className="cursor-pointer" onClick={() => setDark(!dark)}>
      {dark ? <Sun className="h-[1.2rem] w-[1.2rem]" /> : <Moon className="h-[1.2rem] w-[1.2rem]" />}
    </div>
  );
}

Step 4: Test your Dark Mode with styles in CSS/SCSS

Finally, update your CSS or SASS to include styles for dark mode. You can use TailwindCSS's @apply directive to apply classes conditionally.

body {
  @apply text-black min-h-screen dark:bg-slate-900 dark:text-white;
}

And that's it! You now have a fully functional dark mode in your Next.js app, powered by TailwindCSS. The state is persisted using local storage, and users can toggle the mode using a simple UI component.

Guillaume Duhan