Integration Guide

Dark Mode Setup Guide

Everything you need to integrate professional, flicker-free dark mode into your Tailwind CSS v4 project.

1

Inject CSS Variables

Paste your generated theme variables into your global CSS file. This establish the semantic tokens for both modes.

STEP 1
css
@theme {
  /* BRAND */
  --color-brand: var(--brand-default);
  --color-brand-dark: var(--brand-dark);
  --color-brand-light: var(--brand-light);
}

:root {
  /* These variables are kept as aliases for the FOUC prevention script and logic */
  --brand-default: #008294;
  --brand-dark: #006b7d;
  --brand-light: #50b2c5;
}

.dark {
  --brand-default: #0a191b;
  --brand-dark: #008294;
  --brand-light: #83d4e4;

  /* ... rest of your dark shades */
}
2

Prevention Script

Add this minimal script to your <head> to prevent that annoying white flash when users refresh in dark mode.

STEP 2
html
<script>
  if (localStorage.theme === 'dark' || (!('theme' in localStorage) && 
      window.matchMedia('(prefers-color-scheme: dark)').matches)) {
    document.documentElement.classList.add('dark')
  } else {
    document.documentElement.classList.remove('dark')
  }
</script>
3

The Theme Toggle

Create a highly professional, hydration-safe React component to let users switch between modes. This prevents visual flickering and persists their choice to localStorage.

STEP 3
tsx
import { useEffect, useState } from 'react';
import { Sun, Moon } from 'lucide-react';

export const ThemeToggle = () => {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  const [mounted, setMounted] = useState(false);

  // 1. Avoid hydration mismatches by syncing state on mount
  useEffect(() => {
    setMounted(true);
    const isDark = document.documentElement.classList.contains('dark');
    setTheme(isDark ? 'dark' : 'light');
  }, []);

  const toggleTheme = () => {
    const nextTheme = theme === 'light' ? 'dark' : 'light';
    const root = document.documentElement;
    
    if (nextTheme === 'dark') {
      root.classList.add('dark');
      localStorage.theme = 'dark';
      setTheme('dark');
    } else {
      root.classList.remove('dark');
      localStorage.theme = 'light';
      setTheme('light');
    }
  };

  // Render a clean placeholder skeleton until hydration is complete
  if (!mounted) {
    return <div className="w-10 h-10 rounded-xl bg-bg-sunken animate-pulse" />;
  }

  return (
    <button
      onClick={toggleTheme}
      aria-label="Toggle Theme"
      className="p-2.5 rounded-xl border border-border-subtle bg-bg-surface text-text-body hover:scale-105 transition-all shadow-sm cursor-pointer"
    >
      {theme === 'dark' ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
    </button>
  );
};
4

Use Semantic Tokens

Stop using dark:bg-black. Instead, use the semantic variables you defined in Step 1. They handle the flip automatically.

STEP 4
tsx
// ✅ THE RIGHT WAY
<div className="bg-bg-page text-text-body">
  Theme-aware content
</div>

// ❌ THE OLD WAY
<div className="bg-white text-black dark:bg-black dark:text-white">
  Harder to maintain
</div>

The "Flicker-Free" Secret

Most developers fail dark mode because they run the theme script *inside* their React component. This causes a white flash before React hydrates. By putting the minimal script in your head, the browser applies the dark theme before it even renders the first pixel.

Ready to generate your dark mode variables?