ReactTailwind CSS with React

Tailwind CSS with React

Tailwind CSS is a utility-first CSS framework: instead of writing custom class names in a stylesheet, you compose small, single-purpose utility classes directly in your JSX. In 2024–2025 Tailwind has become the dominant styling approach for new React applications, powering component ecosystems like Shadcn/ui, Radix UI Themes, Headless UI, and Catalyst.

The Utility-First Philosophy

Traditional CSS starts with a blank stylesheet and accumulates custom rules. The utility-first approach starts with a large set of predefined atomic classes and composes them. The difference becomes clear on a simple card component:

JSX
/* Traditional CSS approach */
// card.css:  .card { padding: 16px; border-radius: 8px; ... }
// JSX:       <div className="card">

/* Tailwind approach — no separate CSS file needed */
<div className="p-4 rounded-lg bg-white shadow-md border border-gray-100">
  <h2 className="text-xl font-semibold text-gray-900 mb-2">Hello</h2>
  <p className="text-gray-500 text-sm">Some description here.</p>
</div>

The p-4 class applies padding: 1rem, rounded-lg applies border-radius: 0.5rem, and so on. Every class has a single, predictable CSS output. You never leave the JSX file to style the component.

Setup with Vite

Bash
# Create a new Vite + React project
npm create vite@latest my-app -- --template react-ts
cd my-app

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

# Generate config files
npx tailwindcss init -p

The tailwind.config.js file tells Tailwind which files to scan for class names. Classes that are not referenced in those files are stripped from the production build, keeping the final CSS tiny:

JS
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    './index.html',
    './src/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      // Add custom colors, spacing, fonts here
      colors: {
        brand: {
          50:  '#eff6ff',
          500: '#0070f3',
          700: '#0051a2',
        },
      },
    },
  },
  plugins: [],
}

CSS
/* src/index.css — the only CSS file you need */
@tailwind base;
@tailwind components;
@tailwind utilities;

TSX
// src/main.tsx
import './index.css'
import ReactDOM from 'react-dom/client'
import App from './App'

ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
A Real Card Component

Here is a product card that demonstrates Tailwind's most common patterns: layout with flexbox/grid, spacing, typography, color, hover states, and responsive design.

TSX
interface ProductCardProps {
  title: string
  description: string
  price: number
  imageUrl: string
  badge?: string
}

export function ProductCard({ title, description, price, imageUrl, badge }: ProductCardProps) {
  return (
    <div className="group rounded-xl border border-gray-200 bg-white shadow-sm hover:shadow-md transition-shadow duration-200 overflow-hidden">
      {/* Image */}
      <div className="relative aspect-video overflow-hidden bg-gray-100">
        <img
          src={imageUrl}
          alt={title}
          className="h-full w-full object-cover group-hover:scale-105 transition-transform duration-300"
        />
        {badge && (
          <span className="absolute top-2 left-2 rounded-full bg-blue-600 px-2.5 py-0.5 text-xs font-semibold text-white">
            {badge}
          </span>
        )}
      </div>

      {/* Content */}
      <div className="p-4">
        <h3 className="text-lg font-semibold text-gray-900 line-clamp-1">{title}</h3>
        <p className="mt-1 text-sm text-gray-500 line-clamp-2">{description}</p>

        <div className="mt-4 flex items-center justify-between">
          <span className="text-2xl font-bold text-gray-900">
            ${price.toFixed(2)}
          </span>
          <button className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700 active:scale-95 transition-all">
            Add to cart
          </button>
        </div>
      </div>
    </div>
  )
}
Conditional Classes with clsx / cn

Conditional class composition is one of Tailwind's rougher edges. Template literals become unreadable. The solution is the clsx library paired with tailwind-merge (which deduplicates conflicting Tailwind classes). The combination is typically aliased as cn:

Bash
npm install clsx tailwind-merge

TS
// src/lib/utils.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

TSX
import { cn } from '@/lib/utils'

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
  size?: 'sm' | 'md' | 'lg'
}

export function Button({ variant = 'primary', size = 'md', className, ...props }: ButtonProps) {
  return (
    <button
      className={cn(
        // Base styles
        'inline-flex items-center justify-center rounded-lg font-semibold transition-all active:scale-95',

        // Size
        size === 'sm' && 'h-8 px-3 text-xs',
        size === 'md' && 'h-10 px-4 text-sm',
        size === 'lg' && 'h-12 px-6 text-base',

        // Variant
        variant === 'primary' && 'bg-blue-600 text-white hover:bg-blue-700',
        variant === 'secondary' && 'border border-blue-600 text-blue-600 hover:bg-blue-50',
        variant === 'ghost' && 'text-gray-600 hover:bg-gray-100',
        variant === 'danger' && 'bg-red-600 text-white hover:bg-red-700',

        // Disabled
        'disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100',

        // Allow consumer to override
        className,
      )}
      {...props}
    />
  )
}
Note
`tailwind-merge` is crucial when you let consumers pass a `className` prop to override styles. Without it, `cn('bg-blue-600', 'bg-red-500')` would produce both classes and the winner is CSS-order-dependent. With `twMerge`, the last conflicting class wins — deterministic and predictable.
@apply: Extracting Repeated Patterns

When the same combination of utilities appears in many places, you can extract it into a named CSS class using @apply. Use this sparingly — it re-introduces the maintenance burden of custom CSS:

CSS
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .btn-primary {
    @apply inline-flex items-center rounded-lg bg-blue-600 px-4 py-2
           text-sm font-semibold text-white hover:bg-blue-700
           transition-colors disabled:opacity-50;
  }

  .input-base {
    @apply w-full rounded-lg border border-gray-300 px-3 py-2 text-sm
           placeholder:text-gray-400 focus:border-blue-500 focus:outline-none
           focus:ring-2 focus:ring-blue-500/20;
  }
}
Dark Mode

Tailwind's dark mode adds a dark: variant prefix. Configure the strategy in tailwind.config.js'media' respects the OS preference, 'class' toggles dark mode by adding a dark class to the <html> element:

JS
// tailwind.config.js
export default {
  darkMode: 'class',  // or 'media'
  // ...
}

TSX
// Dark mode classes with the 'class' strategy
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 p-6 rounded-xl">
  <h2 className="text-xl font-bold">Hello</h2>
  <p className="text-gray-500 dark:text-gray-400 mt-2">
    This card adapts to dark mode automatically.
  </p>
</div>

// Toggle dark mode:
document.documentElement.classList.toggle('dark')
Responsive Design

Tailwind uses mobile-first breakpoints as prefixes: sm:, md:, lg:, xl:, 2xl:. Write the mobile style first, then override at larger breakpoints:

TSX
// Single column on mobile, 2 columns on tablet, 3 on desktop
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
  {products.map((p) => (
    <ProductCard key={p.id} {...p} />
  ))}
</div>

// Text size scales with screen
<h1 className="text-2xl font-bold sm:text-3xl lg:text-4xl xl:text-5xl">
  Big Heading
</h1>
Why Tailwind Dominates React Projects

Concern

Tailwind advantage

Bundle size

Production CSS is typically 5–15 KB (all unused classes purged)

Runtime cost

Zero — pure static classes, no JS injection

SSR / Server Components

Fully compatible — no runtime needed

Responsive

Built-in breakpoint prefixes, no media query boilerplate

Dark mode

Built-in dark: variant

Design constraints

Spacing/color scale enforces consistency without a design system

Component libraries

Shadcn/ui, Radix Themes, Headless UI all Tailwind-native

  • Start every new React project with Tailwind — it is the industry default in 2025

  • Use cn() (clsx + tailwind-merge) for all conditional class logic

  • Keep @apply minimal — prefer composing utilities in JSX over extracting to CSS

  • Configure your theme in tailwind.config.js instead of using arbitrary values like text-[14px]

  • Install the Tailwind CSS IntelliSense VS Code extension for autocomplete and hover previews

Tip
Shadcn/ui (https://ui.shadcn.com) is a collection of accessible, copy-paste React components built with Tailwind and Radix UI. It is the fastest way to build a polished UI. Run `npx shadcn-ui@latest init` to set it up in an existing Tailwind project.
Warning
Do not use Tailwind's `!important` modifier (the `!` prefix) unless absolutely necessary. It is a signal that your class ordering or specificity is broken, and it makes future overrides much harder.