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.