```
### Typography
```tsx
// Truncate long text
{/* single line ellipsis */}
{/* clamp to 3 lines */}
```
---
## Responsive Design
Tailwind is **mobile-first** — unprefixed utilities apply to all sizes, prefixes override at that breakpoint and up.
```tsx
// Breakpoints: sm(640px), md(768px), lg(1024px), xl(1280px), 2xl(1536px)
// Hide/show
{/* hidden on mobile, visible md+ */}
{/* visible on mobile, hidden md+ */}
// Text size changes per breakpoint
```
---
## Dark Mode
```tsx
// tailwind.config — use 'class' strategy (toggle via class on )
darkMode: 'class'
// Next.js — use next-themes
// In components
```
---
## State Variants
```tsx
// Hover, focus, active
// Group hover — parent hover affects children
// Peer — sibling state
Focused hint
Invalid input
```
---
## The `cn()` Helper
`cn()` merges class names and resolves Tailwind conflicts (last one wins with `tailwind-merge`).
```bash
npm install clsx tailwind-merge
```
```tsx
// lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// Usage — conditional classes, no conflicts
// Conflict resolution — twMerge picks the last conflicting utility
cn("px-4 px-6") // → "px-6"
cn("text-red-500", "text-blue-500") // → "text-blue-500"
```
---
## Component Variants with `cva`
`cva` (class-variance-authority) defines components with typed variant props.
```bash
npm install class-variance-authority
```
```tsx
// components/ui/button.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
// Base styles — always applied
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-blue-600 text-white hover:bg-blue-700",
destructive: "bg-red-600 text-white hover:bg-red-700",
outline: "border border-gray-300 bg-white hover:bg-gray-50",
ghost: "hover:bg-gray-100 hover:text-gray-900",
link: "text-blue-600 underline-offset-4 hover:underline",
},
size: {
sm: "h-8 px-3 text-sm",
default: "h-10 px-4 py-2",
lg: "h-12 px-8 text-lg",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
interface ButtonProps
extends React.ButtonHTMLAttributes
,
VariantProps {
asChild?: boolean
}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
)
}
// Usage
Default
Delete
Cancel
```
---
## Custom Theme Config
```js
// tailwind.config.ts
import type { Config } from "tailwindcss"
export default {
content: ["./src/**/*.{ts,tsx}"],
darkMode: "class",
theme: {
extend: {
colors: {
brand: {
50: "#eff6ff",
500: "#3b82f6",
900: "#1e3a5f",
},
// CSS variable-based (works with shadcn/ui)
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
},
fontFamily: {
sans: ["var(--font-inter)", "sans-serif"],
mono: ["var(--font-mono)", "monospace"],
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
},
keyframes: {
"fade-in": {
from: { opacity: "0", transform: "translateY(4px)" },
to: { opacity: "1", transform: "translateY(0)" },
},
"spin-slow": {
to: { transform: "rotate(360deg)" },
},
},
animation: {
"fade-in": "fade-in 0.2s ease-out",
"spin-slow": "spin-slow 3s linear infinite",
},
},
},
} satisfies Config
```
---
## Animations and Transitions
```tsx
// Transitions — apply to base element, variants add what changes
// All properties
// Transform
// Opacity fade
// Built-in animations
{/* loading spinner */}
{/* skeleton loading */}
{/* custom from config */}
```
---
## Common Component Patterns
### Card
```tsx
```
### Input
```tsx
```
### Badge
```tsx
const badgeVariants = cva(
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
{
variants: {
variant: {
default: "bg-gray-100 text-gray-800",
success: "bg-green-100 text-green-800",
warning: "bg-yellow-100 text-yellow-800",
error: "bg-red-100 text-red-800",
info: "bg-blue-100 text-blue-800",
},
},
defaultVariants: { variant: "default" },
},
)
```
### Modal overlay
```tsx
{/* Backdrop */}
{/* Panel */}
...
```
---
## Tailwind v4 (New in 2025)
```css
/* No tailwind.config — configure in CSS */
@import "tailwindcss";
@theme {
--color-brand-500: #3b82f6;
--font-sans: "Inter", sans-serif;
--radius-lg: 0.75rem;
}
/* Dark mode via media query by default (no class strategy needed) */
/* Use @variant dark { ... } for custom dark styles */
```
v4 changes: config moves to CSS `@theme`, faster build, no PostCSS required, native cascade layers.
---
## Red Flags
- **String template literals for conditional classes** — `` `bg-${color} p-4` `` generates class names at runtime that Tailwind's static scanner never sees and therefore never includes in the output CSS; use only complete class names from the source, with `cn()` for conditions
- **Conflicting utilities without `tailwind-merge`** — `className="px-4 px-6"` applies both; the last rule in the stylesheet wins (not the last in the string), which is unpredictable; `twMerge` resolves conflicts correctly by keeping only the last conflicting utility
- **Repeating variant logic in `if/else` strings** — `className={isActive ? "bg-blue-600 text-white rounded-lg px-4" : "bg-gray-100 text-gray-900 rounded-lg px-4"}` duplicates base classes; use `cva` to declare base + variant styles separately
- **Arbitrary values that are repeated** — `w-[340px]` appearing in five components belongs in `theme.extend` as a named token; arbitrary values are for one-offs only
- **Missing `dark:` variants on color utilities** — adding `bg-white text-gray-900` without corresponding `dark:bg-gray-900 dark:text-gray-100` makes dark mode look broken; pair every color with its dark-mode variant at the same time
- **No `focus-visible:ring` on interactive elements** — keyboard users navigating with Tab have no visible indicator when focus styles are absent; every button and link needs a `focus-visible:ring-*` class for WCAG AA compliance
- **Importing Tailwind classes from JS variables** — storing class names in a variable and spreading them defeats the static content scanner; Tailwind purges any class it cannot find as a complete string in the source at build time
## Checklist
- [ ] `cn()` used instead of string concatenation for conditional classes
- [ ] `cva` used for components with multiple variants (not `if/else` strings)
- [ ] Responsive classes ordered mobile-first (base → sm: → md: → lg:)
- [ ] `group` / `peer` used for parent/sibling state instead of JS state
- [ ] `dark:` variants added alongside every color utility
- [ ] `transition-colors` / `transition-all` on interactive elements
- [ ] `focus-visible:ring` on all interactive elements for keyboard accessibility
- [ ] Arbitrary values (`w-[340px]`) used sparingly — extend theme for repeated values
- [ ] `disabled:` variants on form elements to style disabled state