1K views
Oct 10, 2025
THthemrsami
Setting Up Tailwind CSS in React and Next.js: A Developer's Guide

Setting Up Tailwind CSS in React and Next.js: A Developer's Guide

Image Description
Tailwind CSS versions 3 and 4 into React and Next.js projects, including dark mode configuration and real-world development patterns

A practical walkthrough of integrating Tailwind CSS versions 3 and 4 into React and Next.js projects, including dark mode configuration and real-world development patterns.

I remember the first time I opened a codebase using Tailwind CSS. My immediate reaction was something like "why are there so many classes on every element?" Coming from traditional CSS where you'd write separate stylesheets, it felt backwards. But after building a few components, something clicked. I stopped context-switching between files. I stopped agonizing over class names. I just built things.

This guide emerged from setting up Tailwind across dozens of projects. I'll walk you through both version 3 (which you'll encounter in most existing codebases) and version 4 (the latest release that changes quite a bit under the hood). We'll cover React with Vite and Next.js setups, tackle dark mode implementation properly, and address the gotchas I wish someone had explained to me earlier.

Why Developers Actually Like Tailwind

Traditional CSS follows a familiar pattern: write HTML, jump to a CSS file, create class names, style them, jump back to HTML, realize you need another class, repeat. Tailwind's utility-first approach flips this entirely. You compose styles directly in your markup using pre-built classes that each do one specific thing.

Want some padding? Add

p-4
. Need different padding on larger screens? Make it
md:p-8
. Want to change the text color? Use
text-gray-700
. At first glance, this looks messy. You'll write components with long lists of classes, and your inner voice will scream about separation of concerns. Push through that discomfort for a few hours. You'll discover you're building interfaces significantly faster because you're staying in one mental context.

The clever part happens during your build process. Tailwind scans your entire codebase looking for class names, then generates CSS containing only what you actually used. Despite having thousands of available utilities in Tailwind's design system, your production bundle stays remarkably small.

Getting Started with Tailwind v3 in React

Most React projects these days use Vite rather than Create React App, and for good reason. Vite's development server starts instantly and hot reloading actually feels hot. Let's set up Tailwind v3 in a fresh Vite project.

Start by creating your React project. I typically use TypeScript because it catches so many silly mistakes before runtime:

npm create vite@latest my-react-app -- --template react-ts
cd my-react-app

Now we'll install Tailwind version 3 along with its dependencies. Tailwind v3 relies on PostCSS and Autoprefixer to transform and optimize your CSS during the build process:

npm install -D tailwindcss@3 postcss autoprefixer
npx tailwindcss init -p

That initialization command creates two configuration files for you. The

-p
flag tells it to generate a PostCSS config alongside the Tailwind config, which saves you from creating one manually.

Open the newly created

tailwind.config.js
file. This is where you tell Tailwind where to look for utility classes in your codebase. The configuration uses a content array that defines which files Tailwind should scan:

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      // This is where you'd add custom colors, fonts, spacing, etc.
      // We'll leave it empty for now
    },
  },
  plugins: [],
}

The content paths are absolutely critical. If you add components in a directory that's not covered by these glob patterns, Tailwind won't find your classes and your styles will mysteriously not work. I've spent embarrassing amounts of time debugging styling issues only to realize I forgot to update these paths.

Next, replace everything in

src/index.css
with Tailwind's three essential directives:

@tailwind base;
@tailwind components;
@tailwind utilities;

These Tailwind directives inject different layers of styles into your CSS. The base layer provides sensible defaults and resets. The components layer is where you'd define reusable component classes (though we'll rarely use this). The utilities layer contains all those handy classes like

flex
,
pt-4
, and
text-blue-500
.

Make sure you're importing this CSS file in your

src/main.tsx
:

import './index.css'

Start your development server with

npm run dev
and you're ready to start styling components with Tailwind utilities.

Moving to Tailwind v4 in React

Tailwind v4 launched in late 2024 with a fundamental architectural change. The team rebuilt the engine in Rust, which makes everything faster. More importantly for setup, they eliminated the PostCSS dependency for most use cases. This simplifies configuration considerably.

Start with the same Vite scaffolding:

npm create vite@latest my-react-app -- --template react-ts
cd my-react-app

The installation step is noticeably simpler now:

npm install tailwindcss @tailwindcss/vite

Instead of PostCSS configuration, you integrate Tailwind directly into your Vite setup. Update your

vite.config.ts
file:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

// The Tailwind plugin hooks directly into Vite's build process
export default defineConfig({
  plugins: [
    react(),
    tailwindcss(),
  ],
})

Your

src/index.css
file becomes even simpler:

@import "tailwindcss";

That's genuinely it. No configuration file needed. The new engine automatically discovers your content files, scans them, and generates your CSS. During development, the Rust-based engine rebuilds your styles fast enough that you won't notice any delay when saving files.

Integrating Tailwind v3 with Next.js

Next.js projects have their own quirks, especially with the App Router that shipped in Next.js 13. The setup process differs slightly because of how Next.js structures its directories and handles CSS.

Create your Next.js project with TypeScript and the App Router:

npx create-next-app@latest my-next-app --typescript --app
cd my-next-app

Install the Tailwind dependencies:

npm install -D tailwindcss@3 postcss autoprefixer
npx tailwindcss init -p

Your

tailwind.config.js
needs to account for Next.js's file structure. The App Router uses an
app
directory while older projects use
pages
. I typically include both to ensure compatibility:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",  // for legacy pages directory
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Next.js projects come with a

app/globals.css
file. Replace its contents with Tailwind's directives:

@tailwind base;
@tailwind components;
@tailwind utilities;

This global stylesheet needs to be imported in your root layout at

app/layout.tsx
:

import './globals.css'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

Tailwind v4 Setup in Next.js

Version 4 works differently in Next.js compared to Vite projects. Since Next.js already uses PostCSS internally, you'll use Tailwind's PostCSS plugin rather than a bundler-specific integration.

Create your Next.js project:

npx create-next-app@latest my-next-app --typescript --app
cd my-next-app

Install Tailwind v4 with its PostCSS plugin:

npm install tailwindcss @tailwindcss/postcss

Create a

postcss.config.mjs
file in your project root:

/** @type {import('postcss-load-config').Config} */
const config = {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

export default config;

Update your

app/globals.css
file:

@import "tailwindcss";

The import in your root layout stays the same. The new Tailwind engine automatically discovers your Next.js project structure and finds all your component files without explicit configuration.

Building Components with Tailwind

Once your setup is complete, writing components feels the same whether you're using v3 or v4. Here's a card component that demonstrates common patterns you'll use constantly:

function Card({ title, description, highlighted = false }) {
  return (
    <div className={`
      max-w-sm rounded-lg overflow-hidden shadow-lg p-6
      ${highlighted ? 'bg-blue-50 border-2 border-blue-500' : 'bg-white'}
      hover:shadow-xl transition-shadow duration-300
    `}>
      <h2 className="font-bold text-xl mb-2 text-gray-900">
        {title}
      </h2>
      <p className="text-gray-700 text-base leading-relaxed">
        {description}
      </p>
    </div>
  );
}

This component shows how to handle conditional styling using template literals. When

highlighted
is true, we swap in a blue background and border. Otherwise, we get a plain white card. The hover effect and transition work regardless of the highlighted state because they're always present in the className string.

Making Dark Mode Work Correctly

Dark mode implementation trips up many developers, myself included when I first tried it. The documentation exists but doesn't always explain why you're doing certain steps. Let me walk through both versions clearly.

Dark Mode in Version 3

First, you need to enable class-based dark mode in your

tailwind.config.js
:

module.exports = {
  darkMode: 'class',  // This tells Tailwind to look for a 'dark' class
  // ... rest of your config
}

Now create a theme toggle component. This component needs to manage adding and removing the

dark
class from your HTML element, persist the user's preference, and respect their system preferences if they haven't chosen explicitly:

// components/ThemeToggle.tsx
import { useEffect, useState } from 'react';

export function ThemeToggle() {
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    // First, check if the user has a saved preference
    const savedTheme = localStorage.getItem('theme');
    // If not, check their system preference
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    
    // Use saved preference, fall back to system preference, default to light
    const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
    setTheme(initialTheme);
    
    // Apply the theme by adding/removing the class on the root element
    if (initialTheme === 'dark') {
      document.documentElement.classList.add('dark');
    }
  }, []);

  const toggleTheme = () => {
    const newTheme = theme === 'light' ? 'dark' : 'light';
    setTheme(newTheme);
    
    // Update the DOM immediately so users see the change
    if (newTheme === 'dark') {
      document.documentElement.classList.add('dark');
    } else {
      document.documentElement.classList.remove('dark');
    }
    
    // Save their preference so it persists across sessions
    localStorage.setItem('theme', newTheme);
  };

  return (
    <button
      onClick={toggleTheme}
      className="p-2 rounded-lg bg-gray-200 dark:bg-gray-700 
                 text-gray-900 dark:text-gray-100
                 hover:bg-gray-300 dark:hover:bg-gray-600
                 transition-colors"
      aria-label="Toggle theme"
    >
      {theme === 'light' ? '🌙' : '☀️'}
    </button>
  );
}

There's one annoying problem with this approach. When someone visits your site with dark mode enabled, there's a brief flash where the page loads in light mode before React hydrates and applies the dark class. This happens because the browser renders HTML before JavaScript executes.

The solution is adding a small script that runs before React loads. In Next.js, create a component that injects this script:

// app/ThemeScript.tsx
export function ThemeScript() {
  // This script runs synchronously before the page renders
  const themeScript = `
    (function() {
      const theme = localStorage.getItem('theme');
      const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      
      // Apply dark mode immediately if needed
      if (theme === 'dark' || (!theme && prefersDark)) {
        document.documentElement.classList.add('dark');
      }
    })();
  `;

  return (
    <script dangerouslySetInnerHTML={{ __html: themeScript }} />
  );
}

Include this in your root layout, placing it in the head section so it executes before the body renders:

// app/layout.tsx
import { ThemeScript } from './ThemeScript';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <ThemeScript />
      </head>
      <body>{children}</body>
    </html>
  );
}

Dark Mode in Version 4

Tailwind v4 handles dark mode differently. Instead of a configuration option, you define the behavior using CSS variants. Add this to your global CSS file after importing Tailwind:

@import "tailwindcss";

/* This variant tells Tailwind to apply dark: classes when a parent has the dark class */
@variant dark (&:where(.dark, .dark *));

The theme toggle component works identically to v3. The difference is purely in how Tailwind's engine processes the dark variant during build time.

For a more sophisticated approach that works in both versions, you can use CSS custom properties. This method gives you semantic color names that automatically adapt to the current theme:

@import "tailwindcss";

:root {
  --color-background: 255 255 255;  /* white in RGB */
  --color-text: 17 24 39;           /* gray-900 */
  --color-primary: 59 130 246;      /* blue-500 */
}

.dark {
  --color-background: 17 24 39;     /* gray-900 */
  --color-text: 243 244 246;        /* gray-100 */
  --color-primary: 96 165 250;      /* blue-400, lighter for dark backgrounds */
}

Then extend your Tailwind config to use these custom properties:

module.exports = {
  theme: {
    extend: {
      colors: {
        background: 'rgb(var(--color-background) / <alpha-value>)',
        text: 'rgb(var(--color-text) / <alpha-value>)',
        primary: 'rgb(var(--color-primary) / <alpha-value>)',
      },
    },
  },
}

Now you can write

bg-background
,
text-text
, and
text-primary
throughout your components, and they'll automatically adjust when someone toggles dark mode. This approach reduces the number of
dark:
variants you need to write.

Practical Patterns That Actually Help

After building many projects with Tailwind, certain patterns consistently prove their worth while others create more problems than they solve.

Keep your Tailwind configuration minimal. Only extend the theme when you need consistent custom values used across multiple components. For one-off situations, arbitrary values like

bg-[#1da1f2]
work perfectly fine. I've seen configurations balloon to hundreds of lines of custom utilities that nobody remembers exist.

While Tailwind encourages utility classes everywhere, don't feel guilty about creating component classes for truly repeated patterns. Use the @apply directive sparingly, though. If you find yourself using it constantly, you might be fighting Tailwind's philosophy:

@layer components {
  .btn-primary {
    @apply px-4 py-2 bg-blue-500 text-white rounded-lg 
           hover:bg-blue-600 transition-colors;
  }
}

Install the Prettier plugin for Tailwind. It automatically sorts your utility classes in a consistent order, which makes code reviews significantly easier. Without it, everyone orders classes differently and git diffs become noisy.

The official Tailwind CSS IntelliSense extension for VS Code is genuinely essential. It provides autocomplete for class names, hover previews showing the actual CSS, and warnings when you typo a class name. This extension accelerates learning because you can discover utilities without constantly checking documentation.

Remember that Tailwind makes building inaccessible interfaces easy if you're not careful. The utility classes style visual appearance, but they don't add semantic meaning or proper ARIA attributes. Always use semantic HTML elements and test your interfaces with keyboard navigation and screen readers.

Debugging Common Setup Problems

When styles aren't working, I follow this checklist in order because it catches probably 95% of issues:

First, verify your content paths in the configuration file match your actual project structure. If you created a

lib
folder but didn't add it to the content array, none of your components in that folder will have working styles.

Second, confirm you're importing the CSS file in your application's entry point. In React, that's typically

main.tsx
. In Next.js, it's your root layout.

Third, check for typos in class names. The IntelliSense extension helps enormously here because it underlines invalid class names.

Fourth, for v3 specifically, make sure PostCSS is properly configured and the plugins are installed.

Fifth, clear your build cache and restart the development server. Sometimes the build process gets confused, especially when you've changed configuration files.

One particularly frustrating issue happens when styles work in development but break in production builds. This usually means you're dynamically constructing class names. Tailwind's build process scans your files for complete class name strings. If you write something like

text-${color}-500
, Tailwind can't detect what specific classes to include. Either use complete class names or add them to the safelist in your config.

Resources for Continued Learning

The official Tailwind CSS documentation remains your best resource for understanding individual utilities and configuration options. The docs are genuinely well-written and include searchable examples.

For Next.js-specific guidance, the Next.js styling documentation covers integration details and common patterns.

Tailwind UI is a commercial component library from the Tailwind team. Even if you don't purchase it, studying the free examples teaches you solid patterns for component architecture and responsive design.

Stay current with v4 developments by following the Tailwind Labs GitHub repository and reading their official blog. The team ships regular updates and writes excellent explanatory posts about new features.

Final Thoughts

Tailwind fundamentally changed how I build user interfaces. Whether you stick with the stable version 3 or adopt version 4's improvements, you're working with a tool that genuinely scales from quick prototypes to large production applications.

Success with Tailwind isn't about memorizing every utility class. That's impossible and unnecessary. Instead, internalize the mental model of composing designs from small, purposeful pieces. Start with basic layout utilities like flexbox and spacing. Gradually explore responsive design, hover states, and transitions. When you see a website you admire, inspect it and learn from their approaches.

Remember that Tailwind is a tool, not a dogma. Combine it with other approaches when they serve your project better. Use CSS modules for complex animations. Write custom CSS when you need precise control. Your goal is shipping excellent user experiences, and Tailwind is simply one powerful way to get there more quickly.

TH
themrsami

themrsami

👋 Hello there! I'm themrsami, a passionate coder dedicated to creating easy-to-understand education for all.

More Articles