Back to blog

How to Implement Dark Mode in Tailwind CSS and React: A Complete Guide

Dark mode is no longer just a visual preference; it is a standard design requirement for modern web applications. It reduces eye strain in low-light environments, saves battery life on OLED screens, and provides a sleek, premium aesthetic.

Implementing dark mode requires coordination between the operating system preferences, manual user choices, state persistence (so the choice is remembered on return visits), and preventing the dreaded Flash of Light (where the screen flashes white during page load).

In this guide, we will implement a robust dark mode toggle using Tailwind CSS and React, complete with theme persistence and首屏 (first-screen) flash mitigation.

The Two Approaches to Dark Mode

1. Pure CSS (Media Queries)

The simplest way to support dark mode is using the CSS @media (prefers-color-scheme: dark) media query. The browser automatically inspects the user's operating system settings and applies the styles.

body {
  background-color: #ffffff;
  color: #000000;
}

@media (prefers-color-scheme: dark) {
  body {
    background-color: #0f172a;
    color: #f8fafc;
  }
}
  • Pros: Simple, requires zero JavaScript, zero layout flash.
  • Cons: The user cannot toggle the mode manually inside your app; they are forced to change their system settings.

2. Class-Based Toggling (Recommended)

To allow manual overrides, you use a class-based approach. You check the system preferences, save the state in localStorage, and toggle a .dark class on the root <html> element. Tailwind CSS supports this pattern natively.

Step 1: Configure Tailwind CSS

Update your tailwind.config.js to enable class-based dark mode:

// tailwind.config.js
module.exports = {
  darkMode: 'class', // Enable class-based triggering
  theme: {
    extend: {},
  },
  plugins: [],
}

Now, you can prefix any style with dark: to apply it when the root <html> contains the dark class:

<div class="bg-white text-black dark:bg-slate-900 dark:text-white">
  <p>This text adjusts automatically based on the active theme.</p>
</div>

Step 2: Preventing the First-Screen Page Flash

If your application uses server-side rendering (SSR) or loads heavy client JavaScript, the browser will render the default light HTML before executing your theme checks, causing a bright white flash.

To prevent this, place a small, blocking inline script at the very top of your document’s <head> element. This script will execute before the HTML body starts rendering, injecting the class instantly:

<!-- Place inside head element -->
<script>
  // Check localStorage or system theme preference
  if (
    localStorage.getItem('theme') === 'dark' ||
    (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
  ) {
    document.documentElement.classList.add('dark');
  } else {
    document.documentElement.classList.remove('dark');
  }
</script>

Since this script is inline and blocks body parsing briefly, the browser applies the dark theme before the first pixel paints, ensuring a seamless visual transition.

Step 3: Creating the React Toggle Component

Now, build a React component to allow users to switch themes dynamically:

import React, { useEffect, useState } from 'react';

export function ThemeToggle() {
  const [theme, setTheme] = useState(() => {
    if (typeof window !== 'undefined') {
      return document.documentElement.classList.contains('dark') ? 'dark' : 'light';
    }
    return 'light';
  });

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

  return (
    <button
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      class="p-2 bg-slate-200 dark:bg-slate-800 rounded-lg"
      aria-label="Toggle Theme"
    >
      {theme === 'dark' ? '☀️ Light Mode' : '🌙 Dark Mode'}
    </button>
  );
}

This component initializes state by inspecting the root class, writes updates to the document, and persists the setting to localStorage.

Conclusion

A successful dark mode implementation requires more than just styling classes. By configuring Tailwind's class mechanism, managing states using React useEffect loops, storing preferences in localStorage, and using inline script blockers in the HTML head, you can deliver a premium, seamless dark mode experience that respects user choices without jarring white screen flashes.