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/viteplugin (Tailwind v4) replaces the oldertailwind.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;
}
| Approach | Best for | Trade-off |
|---|---|---|
| Component extraction | Reusable UI with props/state | Slight indirection; the React way |
@apply | Tiny global primitives, third-party markup | Reintroduces CSS files and naming |
| Inline utilities | Most one-off markup | Long class strings on complex nodes |
Avoid
@applyas 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
@applywhenever the pattern has behavior, props, or repeats meaningfully. - Use
clsxfor conditional and multi-state class logic instead of nested template literals. - Define brand colors, fonts, and spacing in
@themeso utilities likebg-brandstay consistent across the app. - Accept a
classNameprop 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).