Tailwind CSS

Status: 🟩 COMPLETE (🟦 LIVING) Last updated: 2026-06-20 Plain-English tagline: A utility-first CSS framework. Instead of writing .button { padding: 16px; ... } in a separate file, you compose styles directly in your HTML/JSX with classes like p-4 bg-blue-500 text-white.


In plain English

In traditional CSS, you write .button-primary somewhere, then add class="button-primary" to elements. Two files, two places to keep in sync, and naming things is hard.

Tailwind flips this. Instead of naming custom classes, you compose styles directly with pre-defined utility classes:

<button class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
  Click me
</button>

Each class does ONE thing: px-4 is “horizontal padding 4 (16px)”, bg-blue-600 is “background color blue-600”, hover:bg-blue-700 is “background blue-700 on hover.”

It looks ugly at first. After a week of use, most developers don’t want to go back. Speed, consistency, and no naming overhead make it the dominant styling approach in modern React/Next.js work in 2026.

Created by Adam Wathan. Open source. Tailwind v4 is current as of mid-2026, with significant changes from v3 — but most projects still target v3 conventions and migrate gradually.


Why it matters

  • Speed. You stop context-switching between HTML and CSS files. Style as you build.
  • No naming. “What do I call this class?” is a real cognitive cost. Tailwind removes it.
  • Consistency. The design system (spacing, colors, font sizes) is baked into the utility names. p-4 is always 16px; you can’t accidentally make it 17px.
  • Smaller production CSS. Tailwind generates CSS only for the classes you actually use. A complex app’s CSS bundle is often smaller than a hand-written one.
  • No conflicts. Two developers writing .card differently can’t clash if neither writes a .card class.
  • shadcn/ui is built on Tailwind. If you use shadcn (recommended), Tailwind is along for the ride.

A 5-minute tour

Spacing

<div class="p-4">      <!-- padding: 16px all sides -->
<div class="px-4 py-2"> <!-- padding 16px horizontal, 8px vertical -->
<div class="mt-8">      <!-- margin-top: 32px -->
<div class="gap-2">     <!-- gap (for flex/grid): 8px -->

The number scale: 1 = 4px, 2 = 8px, 3 = 12px, 4 = 16px, 8 = 32px, 16 = 64px. Always 4px-based. You internalize this fast.

Colors

<div class="bg-blue-500">    <!-- background: blue (medium) -->
<div class="text-slate-900"> <!-- text color: slate (dark) -->
<div class="border-gray-200"> <!-- border color: gray (light) -->

Color scale: 50 (very light) → 100 → 200 → … → 900 → 950 (very dark). Common base colors: slate, gray, zinc, red, orange, yellow, green, blue, purple, pink. Plus white, black, transparent.

Layout

<div class="flex items-center justify-between gap-4">
  <div>Left</div>
  <div>Right</div>
</div>
 
<div class="grid grid-cols-3 gap-4">
  <Card /> <Card /> <Card />
</div>

Flexbox and Grid wrapped in utility classes. Once memorized, you write layouts faster than typing CSS.

Typography

<h1 class="text-3xl font-bold leading-tight">Title</h1>
<p class="text-base text-slate-600 leading-relaxed">Body</p>

text-xs/sm/base/lg/xl/2xl/3xl/4xl… font-thin/light/normal/medium/semibold/bold/black. leading-tight/snug/normal/relaxed/loose.

Responsive

<div class="text-sm md:text-base lg:text-lg">
  Small on mobile, medium on tablet, large on desktop
</div>

Prefixes: sm: (640px+), md: (768px+), lg: (1024px+), xl: (1280px+), 2xl: (1536px+). Mobile-first by default — base styles apply everywhere, prefixed styles override at that breakpoint and up.

Interactive states

<button class="bg-blue-500 hover:bg-blue-700 focus:ring-2 active:bg-blue-800 disabled:opacity-50">
  Click
</button>

Prefixes for states: hover:, focus:, focus-within:, active:, disabled:, group-hover: (responds to parent hover), and many more.

Dark mode

<div class="bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100">
  Adapts to dark mode
</div>

The dark: prefix activates when <html class="dark"> is set. See How-to: Set up dark mode.


How Tailwind actually works

Tailwind is a build-time tool. During npm run build:

  1. Tailwind scans your project files (./app/**/*.{tsx,ts} etc., configured in tailwind.config.ts).
  2. For each class it finds (p-4, bg-blue-500, etc.), it generates the corresponding CSS.
  3. Unused utilities are excluded — your CSS bundle contains only what you used.

The result is a small CSS file (often 10–50KB compressed, even for complex apps) with no manual class authoring.


Configuration

tailwind.config.ts (v3 conventions):

import type { Config } from "tailwindcss";
 
const config: Config = {
  content: [
    "./app/**/*.{ts,tsx}",
    "./components/**/*.{ts,tsx}",
  ],
  darkMode: "class",
  theme: {
    extend: {
      colors: {
        brand: "#FF6B35",
        "brand-dark": "#CC5429",
      },
      fontFamily: {
        sans: ["Inter", "system-ui", "sans-serif"],
      },
    },
  },
  plugins: [],
};
 
export default config;
  • content — where Tailwind looks for classes
  • darkMode — "class" is the modern default
  • theme.extend — your custom additions (extend) or overrides (no extend)
  • plugins — extra functionality

In Tailwind v4, configuration moves to CSS files via @theme directives. The patterns differ; both work; v3 syntax is still most common in mid-2026.


A concrete example: a polished button

<button class="
  px-4 py-2
  bg-blue-600 hover:bg-blue-700 active:bg-blue-800
  text-white font-medium
  rounded-lg
  shadow-sm hover:shadow
  transition-all
  disabled:opacity-50 disabled:cursor-not-allowed
  focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
">
  Save changes
</button>

Looks busy. But:

  • It works
  • It’s accessible (focus ring)
  • It handles hover, active, disabled states
  • It’s keyboard-navigable
  • The result is roughly 40 lines of CSS — generated automatically

In hand-written CSS, this would be:

.btn-primary {
  padding: 8px 16px;
  background: #2563eb;
  color: white;
  font-weight: 500;
  border-radius: 8px;
  box-shadow: 0 1px 2px rgba(0,0,0,0.05);
  transition: all 0.15s;
}
.btn-primary:hover { background: #1d4ed8; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.btn-primary:active { background: #1e40af; }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary:focus { outline: none; box-shadow: 0 0 0 3px white, 0 0 0 5px #3b82f6; }

Both work. Tailwind eliminates the separate file + the naming + the CSS authoring; you read it inline.


Extracting components — when classes get long

When a button is used 50 times, you’d hate to retype 10 classes everywhere. Two approaches:

React component

function Button({ children, ...rest }: ButtonProps) {
  return (
    <button {...rest} className="px-4 py-2 bg-blue-600 ...">
      {children}
    </button>
  );
}

Use everywhere as <Button>Save</Button>. Pure JSX; nothing Tailwind-specific.

@apply (Tailwind directive)

.btn-primary {
  @apply px-4 py-2 bg-blue-600 hover:bg-blue-700 ...;
}

Then <button class="btn-primary">. Less common in React projects; component extraction is preferred.


shadcn/ui — components built on Tailwind

ui is a library of beautiful, accessible React components that you copy into your project (rather than installing as a package). Built on Tailwind + Radix UI.

npx shadcn@latest add button

Drops components/ui/button.tsx into your project. You customize it freely from there. The default styles use Tailwind under the hood.

For modern React apps, the pattern is: Tailwind for layout/custom styling + shadcn/ui for components. Together they’re the fastest path from idea to polished UI.


Common gotchas

  • Class string ordering. Order doesn’t matter for CSS specificity (Tailwind sorts internally) but matters for readability. Use the official Tailwind sort plugin or just write them grouped (flex items-center ... bg-white ... text-slate-900 ... hover:...).

  • Dynamically constructed class names don’t work. Tailwind scans your source files for class names. text-${color}-500 won’t be detected. Use a lookup map: { red: "text-red-500", blue: "text-blue-500" }[color].

  • The content array must match where your files live. If you add a new folder, update content in the config or those files’ classes won’t be generated.

  • @apply in component CSS doesn’t include responsive/hover variants by default. Use @apply hover:bg-blue-700 carefully; sometimes needs @layer components.

  • Conflicting utilities. p-4 p-8 — which wins? The later one, but it’s unclear when wrapped across components. Use tailwind-merge library to deduplicate at runtime if you compose classes from props.

  • Specificity issues with shadcn/ui or Radix. Component libraries may need higher specificity to override. Use !important sparingly or restructure.

  • Production CSS missing classes. Build output looks right in dev but classes don’t apply in production. Usually: file path not in content, or class name dynamically constructed.

  • Tailwind v3 → v4 migration. v4 has significant changes (CSS-first config, new variants, performance). Plan migrations deliberately; don’t auto-upgrade major versions.

  • space-x-* vs gap-*. space-x-4 adds margin to children; gap-4 is grid/flex gap. gap-* is more reliable and works in flex/grid both.

  • Dark mode class on <html> vs <body>. Tailwind looks for dark class on the same level or above. Class on <html> (the recommended approach) works everywhere.

  • Inline classes get long. 15-class buttons happen. Refactor to components or accept the verbosity.

  • Tailwind alone doesn’t give you components. It’s a styling system, not a UI library. Use shadcn/ui, Radix, or build your own.


See also

Sources