A practical guide to setting up Tailwind CSS in modern React and Next.js projects, covering both version 3 and 4 installations, solving the dark mode implementation puzzle, and sharing real-world patterns that actually help you build faster.
The first time I tried Tailwind, I stared at someone's code with about fifteen class names on a single div and thought "this is madness." Two weeks later, I was building faster than I ever had with traditional CSS. Funny how that works.
If you're setting up Tailwind for the first time, you'll probably hit the same confusing spots I did, especially around which version to use and how to make dark mode actually work. Let me save you some Googling.
Why Tailwind Feels Weird at First
Traditional CSS has you writing in separate files, coming up with clever class names, then wondering six months later what .card-wrapper-container
was supposed to mean. Tailwind flips this entirely by letting you compose styles directly in your markup using pre-built utility classes.
Want padding? Add p-4
. Different padding on tablets? Use md:p-6
. Change text color? Try text-slate-700
. Your first reaction will be "wow, this looks messy." Push through that feeling for an afternoon of building something real. You'll notice you're staying in one mental context instead of jumping between files, and that speeds up everything.
The clever bit happens during your build. Tailwind scans your entire codebase for class names and generates CSS containing only what you actually used. Despite having thousands of utilities available in Tailwind's system, your production bundle stays surprisingly lean.
Should You Use Version 3 or 4?
Version 3 is what you'll find in most production codebases right now. It works great and isn't going anywhere soon. Version 4 launched late last year with a Rust-based engine that makes builds faster and simplifies setup by removing the PostCSS requirement in most cases.
My advice? Use v3 if you're working on an existing project or collaborating with a team that hasn't upgraded yet. Choose v4 for new personal projects where you want the latest improvements. Both versions work nearly identically once installed, so switching later isn't painful.
Setting Up in React with Vite
I'll show you v4 since it's simpler. Start by creating your project:
npm create vite@latest my-app -- --template react-ts
cd my-app
Install Tailwind with its Vite plugin:
npm install tailwindcss @tailwindcss/vite
The magic happens in your vite.config.ts
file. You're telling Vite to process Tailwind during the build:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(),
tailwindcss(), // This hooks Tailwind into Vite's build pipeline
],
})
Now update your src/index.css
file to just this single line:
@import "tailwindcss";
That's genuinely it for v4. No config file needed. The new engine automatically finds your component files and scans them. Start your dev server with npm run dev
and you can immediately use classes like bg-blue-500
or rounded-lg
in your components.
Next.js Setup
Next.js works a bit differently because it has PostCSS built in already. Create your Next.js project:
npx create-next-app@latest my-app --typescript --app
cd my-app
Install Tailwind with its PostCSS plugin:
npm install tailwindcss @tailwindcss/postcss
Create a postcss.config.mjs
file in your project root:
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
Update your app/globals.css
:
@import "tailwindcss";
The Next.js documentation covers additional patterns, but this gets you running. The import in your root layout stays the same, and Tailwind automatically discovers your Next.js project structure.
Making Dark Mode Actually Work
Dark mode trips everyone up at first because you need to coordinate between Tailwind's CSS system and JavaScript that toggles classes. Here's the clearest path forward.
First, tell Tailwind to watch for a dark
class. In your global CSS file (after the Tailwind import), add this:
@import "tailwindcss";
/* This tells Tailwind: apply dark: variants when a parent element has the dark class */
@variant dark (&:where(.dark, .dark *));
Now create a theme toggle component. This needs to manage three things: checking if the user has a saved preference, respecting their system preference if they don't, and updating both the DOM and localStorage when they toggle:
import { useEffect, useState } from 'react';
export function ThemeToggle() {
const [theme, setTheme] = useState('light');
useEffect(() => {
// Check localStorage first
const saved = localStorage.getItem('theme');
// Fall back to system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const initial = saved || (prefersDark ? 'dark' : 'light');
setTheme(initial);
if (initial === 'dark') {
document.documentElement.classList.add('dark');
}
}, []);
const toggle = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
// Update the DOM immediately
document.documentElement.classList.toggle('dark');
// Save for next visit
localStorage.setItem('theme', newTheme);
};
return (
<button
onClick={toggle}
className="p-2 rounded-lg bg-gray-200 dark:bg-gray-800"
>
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
}
There's one annoying problem here. When someone visits your site with dark mode enabled, there's a brief flash of light mode before React loads and runs this code. The browser renders HTML before JavaScript executes.
For Next.js projects, fix this by adding a tiny script that runs before React hydrates. Create a component:
export function ThemeScript() {
const script = `
(function() {
const theme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (theme === 'dark' || (!theme && prefersDark)) {
document.documentElement.classList.add('dark');
}
})();
`;
return <script dangerouslySetInnerHTML={{ __html: script }} />;
}
Include it in your root layout's head:
import { ThemeScript } from './ThemeScript';
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<ThemeScript />
</head>
<body>{children}</body>
</html>
);
}
Now you can use dark mode variants throughout your components like bg-white dark:bg-slate-900
or text-gray-900 dark:text-gray-100
, and they'll switch automatically when someone toggles the theme.
Patterns That Make Life Easier
After building with Tailwind for a while, these practices consistently help.
Install the Prettier plugin for Tailwind. It automatically sorts your classes in a consistent order, which stops git diffs from getting messy when everyone orders utilities differently.
Grab the Tailwind CSS IntelliSense extension for VS Code. It autocompletes class names, shows you the actual CSS on hover, and warns you about typos. This genuinely speeds up learning because you discover utilities without constantly checking docs.
Keep your Tailwind config minimal. Only extend the theme when you need consistent custom values across many components. For one-off situations, arbitrary values like bg-[#1a73e8]
work fine. I've seen configs balloon to hundreds of lines that nobody remembers.
Don't forget accessibility. Tailwind styles visual appearance but doesn't add semantic meaning. Always use proper HTML elements, test with keyboard navigation, and add ARIA attributes where needed.
When Styles Aren't Working
If you're seeing unstyled components, walk through this checklist. It catches most issues.
First, verify your import. Make sure you're importing the CSS file in your app's entry point. In React with Vite, that's src/main.tsx
. In Next.js, it's your root layout.
Second, for v3 setups, confirm PostCSS is configured correctly and the plugins are installed. Version 4 doesn't have this dependency, which is why I recommend it for new projects.
Third, check for typos in class names. The IntelliSense extension helps enormously by underlining invalid classes.
Fourth, clear your build cache and restart the dev server. Sometimes the build process gets stuck, especially after changing config files.
One particularly frustrating bug happens when styles work in development but break in production. This usually means you're dynamically constructing class names like text-${color}-500
. Tailwind scans for complete strings during build. Either use complete class names or add them to the safelist configuration.
Where to Learn More
The official Tailwind documentation is genuinely excellent and searchable. Start there when you need to find specific utilities or understand configuration options.
Tailwind UI is a commercial component library, but even the free examples teach you solid patterns for responsive design and component architecture. I learned a lot just by reading through their example code.
Follow the Tailwind Labs blog to stay current with v4 developments. The team writes clear explanatory posts about new features and design decisions.
The Learning Curve Is Worth It
Tailwind changed how I build interfaces. The first few hours feel awkward because you're fighting muscle memory from traditional CSS. But once the utility-first approach clicks, you'll find yourself prototyping ideas much faster because you're staying in one mental context.
Success with Tailwind isn't about memorizing classes. That's impossible. Instead, internalize the mental model of composing designs from small, purposeful utilities. Start with layout basics like flexbox and spacing. Gradually explore responsive variants and transitions. When you see a site you like, inspect it and learn from their patterns.
Remember that Tailwind is a tool, not a religion. Write custom CSS when you need precise control. Use CSS modules for complex animations. Your goal is building excellent user experiences, and Tailwind is simply one way to get there faster.
About the Author
I'm Usama Nazir, a full stack developer specializing in React, Next.js, and TypeScript. I write about modern web development patterns and share practical insights from real-world projects at usama.codes. When I'm not building scalable web applications, I'm exploring new tools and helping other developers navigate the ever-evolving JavaScript ecosystem.
More from my blog:
- Sora 2 Invite Code: How to Get Access to OpenAI Sora 2 - A complete guide to accessing OpenAI's latest AI video generator
- Check out more articles on React, Next.js, and modern web development
Top comments (1)
Love the tips on Prettier plugin for class sorting and IntelliSense to speed up workflow. Clear advice on version differences and structural setup helps reduce common headaches.
Thanks for this actionable Tailwind CSS tutorial !!