Back to blogs
Tailwind Dark Mode Guide

Tailwind Dark Mode Guide

July 1, 2025

Ashish Gogula Ashish Gogula

1. Install Tailwind

Follow Tailwind’s official installation guide.

If you're using Vite + React, install:

npm install tailwindcss @tailwindcss/vite

2. Configure Tailwind Plugin in Vite

Create or update your vite.config.js:

vite.config.js
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [
    tailwindcss(),
  ],
});
📌 This may vary depending on your framework (e.g., Next.js, Astro, etc.)

3. Import Tailwind into CSS

In your index.css or main.css:

index.css
@import "tailwindcss";

Make sure this CSS is loaded in your main file (main.jsx or main.tsx).

4. Add Dark Mode Support in Tailwind

By default, Tailwind applies dark mode using class. Tailwind v4+ allows advanced variants via CSS:

index.css
@variant dark (&:is(.dark *));

What does @variant dark (&:is(.dark *)) do?

This custom variant targets any element inside .dark class, using CSS :is() pseudo-class.

It enables dark styles globally whenever your <html> has class="dark".

home.jsx
<html class="dark">
  <body>
    <div class="bg-white dark:bg-black text-black dark:text-white">
      Hello in Dark Mode!
    </div>
  </body>
</html>

5. Create useTheme Hook

Inside your src/hooks/useTheme.jsx:

useTheme.jsx
import { useState, useEffect, useCallback } from 'react';

export const useTheme = () => {
  const [isDarkMode, setIsDarkMode] = useState(() => {
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      return savedTheme === 'dark';
    }
    return window.matchMedia('(prefers-color-scheme: dark)').matches;
  });

  const toggleTheme = useCallback(() => {
    setIsDarkMode((prev) => {
      const newMode = !prev;
      const html = document.documentElement;

      if (newMode) {
        html.classList.add('dark');
        localStorage.setItem('theme', 'dark');
      } else {
        html.classList.remove('dark');
        localStorage.setItem('theme', 'light');
      }

      return newMode;
    });
  }, []);

  useEffect(() => {
    const html = document.documentElement;
    if (isDarkMode) {
      html.classList.add('dark');
      localStorage.setItem('theme', 'dark');
    } else {
      html.classList.remove('dark');
      localStorage.setItem('theme', 'light');
    }
  }, [isDarkMode]);

  return {
    theme: isDarkMode ? 'Dark' : 'Light',
    toggleTheme
  };
};

Let's break down how the custom useTheme hook works and why it's built this way
1. Persist Theme Preference

const [isDarkMode, setIsDarkMode] = useState(() => {
  const savedTheme = localStorage.getItem('theme');
  if (savedTheme) {
    return savedTheme === 'dark';
  }
  return window.matchMedia('(prefers-color-scheme: dark)').matches;
});

We’re initializing the theme based on:

  • If the user has previously selected a theme (stored in localStorage).
  • If not, we fall back to the system preference (via window.matchMedia).

This initialization function is only run once, thanks to lazy initialization in useState.

2. Toggling Between Themes

const toggleTheme = useCallback(() => {
  setIsDarkMode((prev) => {
    const newMode = !prev;
    const html = document.documentElement;

    if (newMode) {
      html.classList.add('dark');
      localStorage.setItem('theme', 'dark');
    } else {
      html.classList.remove('dark');
      localStorage.setItem('theme', 'light');
    }

    return newMode;
  });
}, []);

This function toggles the theme and does three things:

  1. Updates the state.
  2. Adds/removes the dark class on the <html> tag.
  3. Updates localStorage with the new preference.

We wrap it in useCallback to avoid unnecessary re-creations on re-render.

3. Syncing DOM with Theme State

useEffect(() => {
  const html = document.documentElement;
  if (isDarkMode) {
    html.classList.add('dark');
    localStorage.setItem('theme', 'dark');
  } else {
    html.classList.remove('dark');
    localStorage.setItem('theme', 'light');
  }
}, [isDarkMode]);

This useEffect runs every time the theme (isDarkMode) changes.It ensures the class on <html> and localStorage are always in sync with the current theme.

4. What the Hook Returns

return {
  theme: isDarkMode ? 'Dark' : 'Light',
  toggleTheme
};

You get two things:

  • theme: a readable value ('Dark' or 'Light') for UI use.
  • toggleTheme: a function to switch between modes (can be used in a button or toggle).

Why We Use the <html> Tag for Theme

Most Tailwind CSS themes apply styles based on the dark class being present on the <html> or <body> tag. Using document.documentElement ensures global control over the theme from the root.

6. Use the Hook in a Toggle Button Component

Here’s how to use the ThemeToggleButton inside your layout or any other page:

ToggleSwitch.jsx
import { Moon, Sun } from 'lucide-react';
import { useTheme } from '../../hooks/useTheme';

const ToggleSwitch = () => {
  const { theme, toggleTheme } = useTheme();

  return (
    <div className="min-h-screen bg-white dark:bg-slate-900 text-black dark:text-white transition-colors duration-300 flex items-center justify-center px-4">
      <div className="max-w-md w-full space-y-6 text-center">
        <button
          onClick={toggleTheme}
          className="mx-auto flex cursor-pointer items-center gap-2 px-4 py-2 rounded-xl bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 transition-colors duration-200"
        >
          {theme === 'Dark' ? <Sun size={18} /> : <Moon size={18} />}
          <span>Switch to {theme === 'Light' ? 'Dark' : 'Light'} Mode</span>
        </button>

        <div className="rounded-xl p-6 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
          <h1 className="text-2xl font-semibold mb-2">Tailwind Dark Mode Demo</h1>
          <p className="text-gray-600 dark:text-gray-300">Current Theme: <strong>{theme}</strong></p>
        </div>
      </div>
    </div>
  );
};

export default ToggleSwitch;
Blog Image
Blog Image
💡 Tip: You can place the toggle button inside your navbar so users can switch themes from anywhere in the app.


🎉 Done!

Your app now supports system-based theme, manual toggle, and Tailwind dark variants — all using Tailwind v4+, Vite, and React!

Blog Image