Skip to content
React rc styling 4 min read

Tailwind CSS with React

Tailwind CSS is a utility-first framework that lets you style React components by composing small, single-purpose classes directly in your className attribute. Instead of writing custom CSS files and inventing class names, you describe layout, spacing, color, and typography inline using a constrained design system. This keeps styles colocated with markup, eliminates dead CSS, and scales remarkably well as a codebase grows because there are no global cascade surprises to reason about.

Setting up Tailwind with Vite

The fastest way to use Tailwind in a modern React project is the official Vite plugin. Starting from a Vite + React app, install the dependencies and register the plugin.

npm create vite@latest my-app -- --template react
cd my-app
npm install tailwindcss @tailwindcss/vite

Add the plugin to your Vite config so Tailwind processes your styles during the build.

// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [react(), tailwindcss()],
});

Finally, import Tailwind once at the top of your global stylesheet (the entry CSS that main.jsx already imports).

/* src/index.css */
@import "tailwindcss";

The @tailwindcss/vite plugin (Tailwind v4) replaces the older tailwind.config.js + PostCSS setup. Configuration now lives in CSS via @theme, so most projects need no separate config file at all.

Utility classes in className

With Tailwind imported, every utility is available as a class. You build up a component’s appearance by combining them. The names are predictable: p- for padding, m- for margin, text- for color and size, bg- for background, flex/grid for layout.

function Card({ title, body }) {
  return (
    <div className="max-w-sm rounded-xl bg-white p-6 shadow-md ring-1 ring-gray-200">
      <h3 className="text-lg font-semibold text-gray-900">{title}</h3>
      <p className="mt-2 text-sm leading-relaxed text-gray-600">{body}</p>
    </div>
  );
}

Responsive and state variants are expressed as prefixes. md: applies a class at the medium breakpoint and up, while hover: and focus: apply on interaction.

function Button({ children }) {
  return (
    <button className="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-400 md:px-6">
      {children}
    </button>
  );
}

Conditional classes

Because className is just a string, you toggle styles with ordinary JavaScript. For a few states a template literal works, but it gets unreadable quickly, so most teams reach for the tiny clsx (or classnames) helper.

npm install clsx
import clsx from "clsx";

function Badge({ status }) {
  return (
    <span
      className={clsx(
        "inline-flex rounded-full px-2 py-1 text-xs font-medium",
        status === "active" && "bg-green-100 text-green-800",
        status === "pending" && "bg-yellow-100 text-yellow-800",
        status === "error" && "bg-red-100 text-red-800"
      )}
    >
      {status}
    </span>
  );
}

clsx skips falsy values, so only the matching branch contributes classes. This pattern keeps conditional styling declarative and readable even with several interacting states.

Extracting components vs @apply

A common worry is that repeating long class strings causes duplication. Tailwind offers two answers, and they are not equal.

The idiomatic solution is to extract a React component. The class list lives in one place, you get props and type safety, and the abstraction is the component itself.

function PrimaryButton({ className, ...props }) {
  return (
    <button
      className={clsx(
        "rounded-md bg-indigo-600 px-4 py-2 text-white hover:bg-indigo-700",
        className
      )}
      {...props}
    />
  );
}

The alternative, @apply, lets you fold utilities into a real CSS class. Reach for it only for small, genuinely reusable primitives that aren’t naturally components.

/* src/index.css */
@import "tailwindcss";

.btn-link {
  @apply font-medium text-indigo-600 underline-offset-4 hover:underline;
}
ApproachBest forTrade-off
Component extractionReusable UI with props/stateSlight indirection; the React way
@applyTiny global primitives, third-party markupReintroduces CSS files and naming
Inline utilitiesMost one-off markupLong class strings on complex nodes

Avoid @apply as a default habit. Overusing it recreates the exact global-CSS problems utility-first was designed to remove.

Why utility-first scales

Traditional CSS grows monotonically: every feature adds rules, and nobody dares delete old ones for fear of breaking something elsewhere. Utility-first inverts this. Styles are local to the JSX that uses them, so deleting a component deletes its styles automatically. The class vocabulary is fixed by the design system, which prevents the slow drift of fifty slightly-different blues. And because the generated stylesheet only contains classes you actually used, the shipped CSS stays tiny regardless of app size.

Best Practices

  • Prefer extracting a React component over @apply whenever the pattern has behavior, props, or repeats meaningfully.
  • Use clsx for conditional and multi-state class logic instead of nested template literals.
  • Define brand colors, fonts, and spacing in @theme so utilities like bg-brand stay consistent across the app.
  • Accept a className prop on reusable components and merge it last so callers can override styling.
  • Lean on responsive (md:, lg:) and state (hover:, focus:, dark:) variants rather than writing custom media queries.
  • Install the Tailwind CSS IntelliSense editor extension for autocomplete and class validation.
  • Keep long class lists readable by ordering them consistently (layout, spacing, color, typography, state).
Last updated June 14, 2026
Was this helpful?