React Tailwind CSS: Utility-First Styling Guide (2026)
Tailwind CSS is a utility-first CSS framework that lets you style components directly in JSX using small, composable class names. Instead of writing custom CSS, you compose utilities like flex, pt-4, text-gray-700 and rounded-lg directly on elements. Tailwind v4 (released 2025) brings a new CSS-based configuration, faster builds, and native CSS variables — making it even more powerful for React projects.
Table of Contents
Setup with Next.js and Vite
Next.js (Tailwind v4 + next 15+):
npx create-next-app@latest my-app --tailwind
# Tailwind is pre-configured
Manual setup for existing projects:
npm install tailwindcss @tailwindcss/vite # Vite plugin (v4)
# or
npm install tailwindcss postcss autoprefixer # PostCSS (v3)
/* app/globals.css — Tailwind v4 */
@import "tailwindcss";
/* Tailwind v3 */
@tailwind base;
@tailwind components;
@tailwind utilities;
Vite setup (tailwind.config.ts for v3):
// tailwind.config.ts
import type { Config } from 'tailwindcss'
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
} satisfies Config
Core Utilities
// Layout
<div className="flex items-center justify-between gap-4">
<div className="grid grid-cols-3 gap-6">
<div className="container mx-auto px-4">
// Spacing
<div className="p-4 m-2 px-6 py-3 mt-8 mb-4">
// Typography
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
<p className="text-base text-gray-600 leading-relaxed">
// Colors and backgrounds
<div className="bg-blue-600 text-white hover:bg-blue-700">
<div className="bg-gradient-to-r from-purple-500 to-cyan-500">
// Borders and radius
<div className="border border-gray-200 rounded-xl shadow-md">
<button className="rounded-full px-6 py-2 border-2 border-indigo-500">
// Sizing
<img className="w-16 h-16 rounded-full object-cover" />
<div className="w-full max-w-2xl min-h-screen">
Responsive Design
Tailwind uses mobile-first breakpoints as prefixes:
// Breakpoints: sm(640px) md(768px) lg(1024px) xl(1280px) 2xl(1536px)
<div className="
grid
grid-cols-1 {/* mobile: 1 column */}
sm:grid-cols-2 {/* ≥640px: 2 columns */}
lg:grid-cols-3 {/* ≥1024px: 3 columns */}
xl:grid-cols-4 {/* ≥1280px: 4 columns */}
gap-4
">
// Show/hide at breakpoints
<nav className="hidden md:flex"> {/* hidden mobile, flex ≥768px */}
<button className="md:hidden"> {/* visible mobile, hidden ≥768px */}
// Typography scaling
<h1 className="text-2xl md:text-4xl lg:text-5xl font-bold">
Dark Mode
// tailwind.config.ts — class-based dark mode (recommended)
export default {
darkMode: 'class',
// ...
}
// Toggle with JS — add/remove 'dark' class on <html>
function ThemeToggle() {
const [dark, setDark] = useState(false)
useEffect(() => {
document.documentElement.classList.toggle('dark', dark)
}, [dark])
return <button onClick={() => setDark(!dark)}>Toggle</button>
}
// Dark mode utilities
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<p className="text-gray-600 dark:text-gray-400">
<button className="bg-indigo-600 dark:bg-indigo-500 hover:bg-indigo-700 dark:hover:bg-indigo-400">
Custom Theme
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
brand: {
50: '#f0f9ff',
500: '#6366f1',
900: '#1e1b4b',
},
surface: '#0d1424',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['Fira Code', 'monospace'],
},
borderRadius: {
'4xl': '2rem',
},
animation: {
'fade-in': 'fadeIn 0.3s ease-in-out',
'slide-up': 'slideUp 0.4s ease-out',
},
keyframes: {
fadeIn: { from: { opacity: '0' }, to: { opacity: '1' } },
slideUp: { from: { transform: 'translateY(10px)', opacity: '0' }, to: { transform: 'translateY(0)', opacity: '1' } },
},
},
},
}
Component Patterns
Extract repetitive class strings with component abstraction:
// Button component with variants
function Button({ variant = 'primary', size = 'md', children, ...props }) {
const base = 'inline-flex items-center justify-center font-semibold rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2'
const variants = {
primary: 'bg-indigo-600 text-white hover:bg-indigo-700 focus:ring-indigo-500',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-gray-500',
}
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
}
return (
<button className={`${base} ${variants[variant]} ${sizes[size]}`} {...props}>
{children}
</button>
)
}
clsx and cva
clsx merges class names conditionally. cva (class-variance-authority) adds typed variant APIs:
npm install clsx tailwind-merge class-variance-authority
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import { cva } from 'class-variance-authority'
// Helper — merges Tailwind classes without conflicts
function cn(...inputs) {
return twMerge(clsx(inputs))
}
// cva — typed variants
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-lg font-semibold transition-colors',
{
variants: {
variant: {
primary: 'bg-indigo-600 text-white hover:bg-indigo-700',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
outline: 'border border-indigo-600 text-indigo-600 hover:bg-indigo-50',
},
size: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
)
function Button({ variant, size, className, ...props }) {
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />
}
// Usage — fully typed
<Button variant="outline" size="lg">Click me</Button>
shadcn/ui Integration
shadcn/ui provides copy-paste components built on Tailwind + Radix UI:
npx shadcn@latest init
npx shadcn@latest add button card dialog input
// Components are added to your source tree — fully customizable
import { Button } from '@/components/ui/button'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
function ProductCard({ product }) {
const [open, setOpen] = useState(false)
return (
<Card>
<CardHeader>
<CardTitle>{product.name}</CardTitle>
</CardHeader>
<CardContent>
<p>${product.price}</p>
<Button onClick={() => setOpen(true)}>View Details</Button>
</CardContent>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{product.name}</DialogTitle>
</DialogHeader>
<p>{product.description}</p>
</DialogContent>
</Dialog>
</Card>
)
}
Tailwind v4 Changes
Tailwind v4 (2025) replaces the JS config with a CSS-based config:
/* globals.css — Tailwind v4 config in CSS */
@import "tailwindcss";
@theme {
--color-brand-500: #6366f1;
--color-brand-900: #1e1b4b;
--font-sans: 'Inter', system-ui, sans-serif;
--radius-xl: 0.75rem;
}
/* Custom utilities */
@utility card {
background: var(--color-surface);
border-radius: var(--radius-xl);
padding: 1.5rem;
}
tailwind.config.ts to CSS-based @theme blocks. The PostCSS plugin is replaced by a Vite plugin for much faster builds.