Skip to main content
Tailwind Dark Mode Guide

Tailwind Dark Mode Guide

July 1, 2025

Ashish Gogula

1. Install Tailwind

Follow Tailwind’s official installation guide (opens in new tab).

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;
💡 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!