Articles › CSS & Web Design
Tailwind CSS Guide: Utility-First Styling for Modern React Apps
Learn Tailwind CSS v4: utility-first styling, responsive and state variants, @theme tokens, dark mode, RTL support and reusable React components.
Tailwind CSS changed how many teams write styles. Instead of inventing class names and switching between JSX and CSS files, you compose small utility classes directly in your markup. At first class="flex items-center gap-3 rounded-xl bg-white p-4 shadow" looks messy; after a week, most developers find they build interfaces faster and their CSS stops growing forever. This guide covers Tailwind v4 in a React + Vite project.
The utility-first idea
Traditional CSS:
<div class="card">...</div>
<style>
.card { display: flex; align-items: center; gap: .75rem; padding: 1rem; border-radius: .75rem; background: white; }
</style>
Tailwind:
<div class="flex items-center gap-3 rounded-xl bg-white p-4">...</div>
Advantages:
- No naming — no more
.card-inner-wrapper-2. - Design constraints — spacing, colors and sizes come from a shared scale, so the UI stays consistent.
- CSS stops growing — you reuse the same utilities; unused ones are never generated.
- Local reasoning — changing a class affects only that element; no fear of breaking other pages.
Setup with Vite (v4)
npm install tailwindcss @tailwindcss/vite
// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({ plugins: [react(), tailwindcss()] });
/* src/index.css */
@import "tailwindcss";
That's all. Tailwind v4 scans your source files automatically — no content array, no tailwind.config.js required.
The essentials
| Purpose | Examples |
|---|---|
| Layout | flex, grid, grid-cols-3, items-center, justify-between, gap-4 |
| Spacing | p-4, px-6, mt-2, space-y-3 |
| Sizing | w-full, max-w-3xl, h-12, size-10 |
| Typography | text-sm, text-2xl, font-semibold, leading-relaxed, text-gray-600 |
| Backgrounds and borders | bg-white, bg-violet-600, border, rounded-xl, ring-1 |
| Effects | shadow-lg, opacity-80, blur-sm, transition, duration-200 |
Arbitrary values cover the edge cases: top-[72px], bg-[#13121f], grid-cols-[200px_1fr].
Responsive design
Tailwind is mobile-first: unprefixed classes apply everywhere, prefixed ones from a breakpoint up.
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{projects.map((p) => <ProjectCard key={p.id} project={p} />)}
</div>
Default breakpoints: sm 40rem, md 48rem, lg 64rem, xl 80rem, 2xl 96rem. v4 also supports container queries out of the box: @container on the parent and @md:flex-row on children.
State variants
<button className="rounded-lg bg-violet-600 px-4 py-2 font-medium text-white
hover:bg-violet-700 focus-visible:outline-2 focus-visible:outline-offset-2
active:scale-95 disabled:cursor-not-allowed disabled:opacity-50">
Save
</button>
Others you'll use often: group-hover: (style a child when its parent is hovered), peer-invalid: (react to a sibling input), first:, last:, odd:, aria-expanded:, data-[state=open]:.
<a className="group flex items-center gap-2">
Read more
<ArrowIcon className="transition group-hover:translate-x-1" />
</a>
Theme tokens with @theme
In v4 the theme lives in CSS. Defining a token generates the utilities automatically:
@import "tailwindcss";
@theme {
--color-brand-50: #f5f3ff;
--color-brand-500: #8b5cf6;
--color-brand-700: #6d28d9;
--font-display: "Sora Variable", sans-serif;
--font-fa: "Vazirmatn Variable", sans-serif;
--radius-card: 1.25rem;
}
Now bg-brand-500, text-brand-700, font-display and rounded-card work everywhere — and the values are also real CSS variables you can use in custom CSS: var(--color-brand-500).
Dark mode
By default dark: follows the operating system (prefers-color-scheme). For a manual toggle, redefine the variant to use a class:
@custom-variant dark (&:where(.dark, .dark *));
<div className="bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-100">...</div>
RTL and Persian sites
Tailwind has logical properties, which flip automatically with dir:
| Physical | Logical (RTL-aware) |
|---|---|
ml-4 / mr-4 |
ms-4 / me-4 |
pl-6 / pr-6 |
ps-6 / pe-6 |
left-0 / right-0 |
start-0 / end-0 |
text-left |
text-start |
rounded-l-lg |
rounded-s-lg |
Prefer logical utilities everywhere and the same component works in both Persian (dir="rtl") and English. For rare cases use the rtl: and ltr: variants: rtl:rotate-180 to flip an arrow icon.
Reusable components in React
The answer to "repeated class lists" is components, not CSS classes:
const variants = {
primary: "bg-violet-600 text-white hover:bg-violet-700",
outline: "border border-gray-300 text-gray-800 hover:bg-gray-50",
ghost: "text-gray-700 hover:bg-gray-100",
};
const sizes = { sm: "h-8 px-3 text-sm", md: "h-10 px-4", lg: "h-12 px-6 text-lg" };
export function Button({ variant = "primary", size = "md", className = "", ...props }) {
return (
<button
className={`inline-flex items-center justify-center gap-2 rounded-lg font-medium transition
disabled:opacity-50 ${variants[variant]} ${sizes[size]} ${className}`}
{...props}
/>
);
}
For conditional classes and safe overrides, combine clsx with tailwind-merge (it resolves conflicts like p-2 vs p-4):
import clsx from "clsx";
import { twMerge } from "tailwind-merge";
export const cn = (...inputs) => twMerge(clsx(inputs));
Warning: Tailwind finds classes by scanning source text. Never build class names dynamically like
`bg-${color}-500`— those classes won't be generated. Map to complete class strings instead.
Custom CSS when you need it
Tailwind doesn't forbid CSS. Use @layer components for things like markdown content, or @utility to create your own utility:
@utility scrollbar-hidden {
scrollbar-width: none;
&::-webkit-scrollbar { display: none; }
}
For long-form article HTML, the official @tailwindcss/typography plugin adds beautiful defaults with a single prose class.
Keeping it maintainable
- Install the Tailwind CSS IntelliSense extension for autocomplete and hover previews.
- Use the official Prettier plugin to sort classes in a consistent order.
- Extract components when a pattern repeats three times.
- Keep design decisions in
@theme, not in arbitrary values scattered everywhere.
Tailwind vs other approaches
| Approach | Pros | Cons |
|---|---|---|
| Tailwind | fast, consistent, tiny CSS | long class lists, learning the names |
| CSS Modules | plain CSS, scoped | naming, more files, CSS grows |
| CSS-in-JS | dynamic styles, colocated | runtime cost (for many libraries) |
| Component libraries | ready-made UI | harder to customize, generic look |
Summary
Tailwind CSS lets you style directly in markup with a constrained design system: mobile-first responsive prefixes, state variants, theme tokens in @theme, logical properties for RTL and a dark: variant. Combine it with React components and cn() to avoid repetition, avoid dynamic class names, and keep custom CSS for the few places that need it.
FAQ
Doesn't Tailwind make HTML messy?
The markup gets longer, but styles stay local and predictable. Extracting components keeps JSX readable.
Is Tailwind bad for performance?
The opposite: only used classes are generated, and production CSS is usually under 20–30 KB uncompressed.
Should I migrate an existing CSS project to Tailwind?
Not necessarily. Tailwind can coexist with existing CSS; use it for new components and migrate gradually if the team likes it.