Tailwind CSS
Tailwind CSS utility-first patterns for React and Next.js applications.
Critical Rules
- Utility-first — compose styles from utility classes directly in markup; extract components, not CSS classes
- Never use
@applyto build component classes — extract a React component instead;@applydefeats the purpose of utility-first and creates abstraction layers that are harder to maintain - Use the
cn()helper for conditional classes — combineclsx+tailwind-mergeto handle conflicts and conditionals cleanly - Mobile-first responsive — unprefixed utilities target mobile; use
sm:,md:,lg:,xl:,2xl:for larger screens - Semantic color tokens — use design-system colors (
bg-primary,text-muted-foreground) rather than raw scales (bg-blue-500) when a design system is in place - Dark mode via
dark:variant — pair light and dark utilities on the same element; never maintain separate component trees for themes
Setup with Next.js
Tailwind v4 (CSS-first configuration)
npm install tailwindcss @tailwindcss/postcss
/* app/globals.css */
@import "tailwindcss";
// postcss.config.ts
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
Tailwind v4 uses CSS for configuration instead of tailwind.config.js. Define custom values with @theme:
/* app/globals.css */
@import "tailwindcss";
@theme {
--color-primary: #2563eb;
--color-primary-foreground: #ffffff;
--color-muted: #f1f5f9;
--color-muted-foreground: #64748b;
--color-destructive: #ef4444;
--font-sans: "Inter", sans-serif;
--font-mono: "JetBrains Mono", monospace;
--radius-lg: 0.75rem;
--radius-md: 0.5rem;
--radius-sm: 0.25rem;
}
These become utilities automatically: bg-primary, text-muted-foreground, font-sans, rounded-lg.
Tailwind v3 (JS configuration)
// tailwind.config.ts
import type { Config } from "tailwindcss";
const config: Config = {
content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"],
darkMode: "class",
theme: {
extend: {
colors: {
primary: { DEFAULT: "#2563eb", foreground: "#ffffff" },
muted: { DEFAULT: "#f1f5f9", foreground: "#64748b" },
},
},
},
};
export default config;
The cn() Utility
Every project using Tailwind in React should have this helper:
// lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
npm install clsx tailwind-merge
Usage — twMerge resolves conflicting utilities so the last one wins:
import { cn } from "@/lib/utils";
<div className={cn(
"rounded-lg border p-4", // base styles
isActive && "border-primary", // conditional
className, // prop override (wins over base)
)} />
Without twMerge, cn("p-4", "p-6") would produce "p-4 p-6" (both applied, unpredictable). With twMerge, it correctly produces "p-6".
Responsive Design
Mobile-first breakpoints — unprefixed utilities are the base (mobile):
| Prefix | Min-width | Target |
|---|---|---|
| (none) | 0px | Mobile (base) |
sm: |
640px | Landscape phones |
md: |
768px | Tablets |
lg: |
1024px | Laptops |
xl: |
1280px | Desktops |
2xl: |
1536px | Large screens |
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{items.map(item => <Card key={item.id} {...item} />)}
</div>
<h1 className="text-2xl font-bold md:text-3xl lg:text-4xl">
Responsive heading
</h1>
Container pattern:
<div className="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8">
{children}
</div>
Dark Mode
Class-based (recommended for Next.js)
Toggle the dark class on <html> — use next-themes for persistence:
npm install next-themes
// app/providers.tsx
"use client";
import { ThemeProvider } from "next-themes";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
);
}
// Usage — pair light and dark utilities:
<div className="bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-100">
<p className="text-gray-600 dark:text-gray-400">Muted text</p>
</div>
CSS variable approach (scales with design systems)
@theme {
--color-background: #ffffff;
--color-foreground: #0a0a0a;
--color-card: #ffffff;
--color-card-foreground: #0a0a0a;
--color-border: #e5e7eb;
}
.dark {
--color-background: #0a0a0a;
--color-foreground: #fafafa;
--color-card: #111111;
--color-card-foreground: #fafafa;
--color-border: #27272a;
}
Then use bg-background, text-foreground, border-border — no dark: prefix needed because the variables themselves change.
Component Patterns
Reusable Button with Variants (CVA)
npm install class-variance-authority
// components/ui/button.tsx
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-muted text-muted-foreground hover:bg-muted/80",
destructive: "bg-destructive text-white hover:bg-destructive/90",
outline: "border border-border bg-transparent hover:bg-muted",
ghost: "hover:bg-muted",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
sm: "h-8 px-3 text-xs",
default: "h-10 px-4",
lg: "h-12 px-6 text-base",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
Card Component
// components/ui/card.tsx
import { cn } from "@/lib/utils";
export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("rounded-lg border border-border bg-card p-6 text-card-foreground shadow-sm", className)}
{...props}
/>
);
}
export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("mb-4 space-y-1.5", className)} {...props} />;
}
export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
return <h3 className={cn("text-lg font-semibold leading-none", className)} {...props} />;
}
export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("text-sm text-muted-foreground", className)} {...props} />;
}
Input Component
// components/ui/input.tsx
import { cn } from "@/lib/utils";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
export function Input({ className, ...props }: InputProps) {
return (
<input
className={cn(
"flex h-10 w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm",
"placeholder:text-muted-foreground",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
);
}
Layout Patterns
Sticky Header + Scrollable Content
<div className="flex h-screen flex-col">
<header className="sticky top-0 z-50 border-b border-border bg-background/80 backdrop-blur-sm">
<nav className="mx-auto flex h-16 max-w-7xl items-center px-4">
{/* nav content */}
</nav>
</header>
<main className="flex-1 overflow-y-auto">
{children}
</main>
</div>
Sidebar Layout
<div className="flex h-screen">
<aside className="hidden w-64 shrink-0 border-r border-border bg-muted/40 lg:block">
{/* sidebar */}
</aside>
<main className="flex-1 overflow-y-auto p-6">
{children}
</main>
</div>
Centered Content
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-6 p-4">
{/* login form, etc. */}
</div>
</div>
Typography
{/* Heading hierarchy */}
<h1 className="text-4xl font-bold tracking-tight">Page title</h1>
<h2 className="text-2xl font-semibold tracking-tight">Section</h2>
<h3 className="text-xl font-semibold">Subsection</h3>
{/* Body text */}
<p className="leading-7 text-muted-foreground">Body copy with relaxed leading.</p>
<p className="text-sm text-muted-foreground">Small helper text.</p>
{/* Prose (for markdown/CMS content) */}
<article className="prose prose-gray dark:prose-invert max-w-none">
{/* rendered markdown */}
</article>
The @tailwindcss/typography plugin provides the prose class for styling rendered HTML/markdown content.
Animation Utilities
{/* Built-in animations */}
<div className="animate-spin" /> {/* loading spinner */}
<div className="animate-pulse" /> {/* skeleton loader */}
<div className="animate-bounce" /> {/* attention indicator */}
{/* Transition utilities */}
<button className="transition-colors duration-200 hover:bg-primary">
Smooth hover
</button>
<div className="transition-all duration-300 ease-out hover:scale-105 hover:shadow-lg">
Card hover effect
</div>
For complex animations, use Framer Motion instead of Tailwind animation utilities.
Anti-Patterns
| Anti-Pattern | Correct Approach |
|---|---|
@apply for component styles |
Extract a React component |
Raw color scales everywhere (bg-blue-500) |
Define semantic tokens (bg-primary) |
| Duplicating class strings across files | Extract a component or use CVA |
className="..." with 20+ utilities on one line |
Break across multiple lines or use cn() with grouped strings |
Inline style={} for spacing/colors |
Use Tailwind utilities |
| Custom CSS file for simple layouts | Compose with flex/grid utilities |
Forgetting dark: variants |
Always pair light and dark styles |
Not using twMerge for overridable components |
Always use cn() for components that accept className |
For spacing, sizing, color, and typography reference tables, see reference/utility-reference.md.
For form styling, table patterns, and advanced layout techniques, see reference/component-patterns.md.