Tailwind With React and Next.js

Tailwind CSS and React are a natural pair. React breaks your UI into small, reusable components, and Tailwind styles each component directly in the markup with utility classes. The result is a fast workflow where you style and build at the same time — no separate stylesheet to maintain, no naming classes, no context switching.

Setting Up Tailwind in a React Project (Vite)

If you are starting a new React project with Vite, the setup takes four steps.

# Step 1: Create the React + Vite project
npm create vite@latest my-app -- --template react
cd my-app

# Step 2: Install Tailwind and its peer dependencies
npm install -D tailwindcss postcss autoprefixer

# Step 3: Generate config files
npx tailwindcss init -p

After step 3, edit tailwind.config.js to tell Tailwind where your component files live:

// tailwind.config.js
module.exports = {
  content: [
    './index.html',
    './src/**/*.{js,ts,jsx,tsx}',
  ],
  theme: { extend: {} },
  plugins: [],
};

Finally, add the three Tailwind directives to your main CSS file (usually src/index.css):

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

Import that CSS file in src/main.jsx:

import './index.css';

Run npm run dev and Tailwind is live.

Setting Up Tailwind in a Next.js Project

Next.js has built-in support for Tailwind CSS. The quickest path is the official create-next-app command, which handles everything automatically.

npx create-next-app@latest my-nextjs-app

During setup, the prompt asks: "Would you like to use Tailwind CSS?" — choose Yes. The CLI installs Tailwind, creates the config files, and adds the directives for you.

If you add Tailwind to an existing Next.js project instead, run:

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

Then update tailwind.config.js to include your Next.js file paths:

content: [
  './app/**/*.{js,ts,jsx,tsx,mdx}',
  './pages/**/*.{js,ts,jsx,tsx,mdx}',
  './components/**/*.{js,ts,jsx,tsx,mdx}',
],

Add the directives to app/globals.css or styles/globals.css and import that file in your root layout.

Diagram: How Tailwind Works in a React/Next.js Build


  .jsx / .tsx component files
  (contain class="flex p-4 bg-blue-500 ...")
          |
          v
  Tailwind content scan
  (reads every file in your content paths)
          |
          v
  Builds only the CSS classes actually used
          |
          v
  Final styles.css  ──► Browser renders UI

Tailwind never ships unused classes to the browser. It reads your component files, finds every class name you used, and generates only those CSS rules. A large project still produces a small CSS file.

Writing Components With Tailwind Classes

Every HTML element inside a React component accepts a className prop. You place Tailwind classes there directly.

function AlertBanner({ message, type }) {
  const base = 'px-4 py-3 rounded-lg text-sm font-medium';
  const styles = {
    success: 'bg-green-100 text-green-800 border border-green-300',
    error:   'bg-red-100 text-red-800 border border-red-300',
    info:    'bg-blue-100 text-blue-800 border border-blue-300',
  };

  return (
    <div className={`${base} ${styles[type]}`}>
      {message}
    </div>
  );
}

Usage:

<AlertBanner message="File saved successfully." type="success" />
<AlertBanner message="Upload failed. Try again." type="error" />

The component controls its own visual logic. The parent just passes a type prop, and the styling changes automatically.

Conditional Classes in React

React lets you compute class names dynamically with JavaScript expressions. A common pattern uses a ternary operator inside the className string.

function Button({ label, disabled }) {
  return (
    <button
      disabled={disabled}
      className={`
        px-5 py-2 rounded font-semibold transition-colors
        ${disabled
          ? 'bg-gray-300 text-gray-500 cursor-not-allowed'
          : 'bg-indigo-600 text-white hover:bg-indigo-700'}
      `}
    >
      {label}
    </button>
  );
}

Using clsx for Cleaner Conditional Classes

String template literals get messy when you have three or more conditions. The clsx library cleans this up. It accepts objects, arrays, and strings, and joins only the truthy ones.

npm install clsx
import clsx from 'clsx';

function Tag({ label, active, size }) {
  return (
    <span
      className={clsx(
        'rounded-full px-3 py-1 text-xs font-medium',
        active ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700',
        size === 'lg' && 'text-sm px-4 py-2',
      )}
    >
      {label}
    </span>
  );
}

clsx only includes the third argument when size === 'lg' is true. There are no empty strings or undefined values in the final class list.

Using tailwind-merge to Avoid Class Conflicts

When a parent component passes extra classes into a child, two conflicting Tailwind classes can land on the same element. For example, p-4 and p-8 — only one can win, but which one wins is unpredictable.

tailwind-merge solves this. It resolves conflicts in favor of the last class in the list, the same logic developers expect.

npm install tailwind-merge
import { twMerge } from 'tailwind-merge';

function Card({ className, children }) {
  return (
    <div className={twMerge('rounded-lg p-4 bg-white shadow', className)}>
      {children}
    </div>
  );
}

// Usage — the parent overrides p-4 with p-8
<Card className="p-8 bg-gray-50">Content here</Card>

The final element gets p-8 and bg-gray-50, not the conflicting defaults. twMerge handles this automatically.

Combining clsx and tailwind-merge

Many React projects combine both libraries into a single helper function, usually named cn.

import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs) {
  return twMerge(clsx(inputs));
}
// Use cn everywhere in your components
<div className={cn('base-classes', isActive && 'active-classes', className)}>

This pattern is standard in popular component libraries like shadcn/ui. You write it once and reuse it across every component in your project.

Tailwind in Next.js: App Router vs Pages Router

Both the App Router (Next.js 13+) and the older Pages Router work with Tailwind the same way. Classes go in className on any JSX element. The only difference is where your global CSS file is imported.

RouterImport CSS inGlobal CSS file
App Routerapp/layout.tsxapp/globals.css
Pages Routerpages/_app.tsxstyles/globals.css

Server Components and Tailwind (Next.js App Router)

React Server Components render on the server and send HTML to the browser. Tailwind works perfectly with them because class names are just strings in your JSX — there is no JavaScript needed at runtime to apply them.

// app/page.tsx — this is a Server Component by default
export default function HomePage() {
  return (
    <main className="min-h-screen bg-slate-50 flex items-center justify-center">
      <h1 className="text-4xl font-bold text-slate-900">
        Welcome to My Site
      </h1>
    </main>
  );
}

Tailwind classes on Server Components are zero JavaScript in the browser. The server renders the HTML with the class names, and the pre-built CSS file handles the visual output.

Diagram: Component Reuse Pattern in React + Tailwind


  Design Token (tailwind.config.js)
  colors.brand = '#4F46E5'
        |
        ▼
  Base Component (Button.jsx)
  ┌──────────────────────────────────┐
  │ className="bg-indigo-600         │
  │            text-white            │
  │            rounded px-4 py-2"    │
  └──────────────────────────────────┘
        |
        ├── <Button>Submit</Button>     → indigo button
        ├── <Button size="lg">          → larger indigo button
        └── <Button variant="ghost">    → transparent button

One component holds the Tailwind logic. Every page in your app calls that component. When you update the component's classes, every instance across the whole project updates at once.

Styling Next.js Link and Image Components

Next.js ships its own <Link> and <Image> components. Both accept className and work with Tailwind directly.

import Link from 'next/link';
import Image from 'next/image';

export default function NavBar() {
  return (
    <nav className="flex items-center justify-between px-6 py-4 bg-white shadow">
      <Link href="/" className="text-xl font-bold text-indigo-600 hover:text-indigo-800">
        MySite
      </Link>
      <Image
        src="/logo.png"
        alt="Logo"
        width={40}
        height={40}
        className="rounded-full"
      />
    </nav>
  );
}

Organizing Styles in Large React Projects

As a project grows, long class strings become hard to read. Three patterns help keep things manageable.

Pattern 1: Extract to a Variable

const cardStyles = 'bg-white rounded-xl shadow-md p-6 hover:shadow-lg transition-shadow';

function ProductCard({ title }) {
  return <div className={cardStyles}>{title}</div>;
}

Pattern 2: Variants Object

const buttonVariants = {
  primary: 'bg-blue-600 text-white hover:bg-blue-700',
  secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
  danger: 'bg-red-600 text-white hover:bg-red-700',
};

function Button({ variant = 'primary', children }) {
  return (
    <button className={`px-4 py-2 rounded font-medium ${buttonVariants[variant]}`}>
      {children}
    </button>
  );
}

Pattern 3: cva (Class Variance Authority)

The cva library builds on this pattern and gives you a structured, type-safe way to define component variants. It is widely used in modern React component libraries.

npm install class-variance-authority
import { cva } from 'class-variance-authority';

const button = cva('px-4 py-2 rounded font-medium transition-colors', {
  variants: {
    intent: {
      primary: 'bg-blue-600 text-white hover:bg-blue-700',
      secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
    },
    size: {
      sm: 'text-sm px-3 py-1',
      lg: 'text-lg px-6 py-3',
    },
  },
  defaultVariants: {
    intent: 'primary',
    size: 'sm',
  },
});

function Button({ intent, size, children }) {
  return <button className={button({ intent, size })}>{children}</button>;
}

Dark Mode in Next.js With Tailwind

Set darkMode: 'class' in your Tailwind config to control dark mode manually. Next.js can toggle a class on the <html> element, and Tailwind dark variants respond to it.

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  // ...
};
// A component that responds to dark mode
<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-white">
  Page content
</div>

Add a toggle button that adds or removes the dark class on document.documentElement, and dark mode switches instantly across the whole app.

Tailwind IntelliSense for React and Next.js

Install the Tailwind CSS IntelliSense extension for VS Code. It provides autocomplete for class names inside className strings, shows the CSS each class generates on hover, and highlights invalid class names.

Add this to your VS Code settings to enable autocomplete inside template literals and helper functions like cn:

{
  "tailwindCSS.experimental.classRegex": [
    ["cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
    ["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
  ]
}

Leave a Comment