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.

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;
}
Migration: Tailwind v4 is largely backward-compatible for class names. The main change is moving from tailwind.config.ts to CSS-based @theme blocks. The PostCSS plugin is replaced by a Vite plugin for much faster builds.