-
Confirm the Tailwind version before writing any class. Open package.json (or run grep -r "tailwindcss" package.json). v4 = tailwindcss@^4, a single @import "tailwindcss"; line in CSS, and NO tailwind.config.js. If you see a tailwind.config.{js,ts} with theme.extend, it's v3 — STOP and ask before migrating; v3/v4 config is not interchangeable.
-
Define tokens once in CSS via @theme (v4 is CSS-first, not JS-config). Put them in the global stylesheet, not scattered per-component:
@import "tailwindcss";
@theme {
--color-bg: oklch(0.18 0.02 265);
--color-surface: oklch(0.22 0.02 265);
--color-accent: oklch(0.62 0.18 265);
--radius-card: 0.75rem;
--font-sans: "Inter", system-ui, sans-serif;
}
Every key under @theme auto-generates utilities (bg-bg, text-accent, rounded-card, font-sans). Use oklch() for color — perceptually uniform, predictable dark-mode shifts. Keep Tailwind's built-in spacing scale (p-4, gap-6); only add tokens for things the default scale lacks.
-
Style mobile-first: unprefixed = mobile base, prefixes = larger screens. Write the small-screen layout with no prefix, then layer sm: md: lg: xl: 2xl: for overrides. Breakpoints are min-width — md:flex-row means "≥768px", NOT "≤". Default breakpoints: sm 640 / md 768 / lg 1024 / xl 1280 / 2xl 1536.
<div class="flex flex-col gap-4 md:flex-row md:gap-6">
-
Pick Flexbox vs Grid deliberately. Flexbox = 1D content that flows/wraps (nav bars, button rows, chip lists) — use flex-wrap + gap. Grid = 2D structured layout (card galleries, dashboards) — prefer grid grid-cols-[repeat(auto-fit,minmax(16rem,1fr))] so it reflows WITHOUT breakpoint classes. Reach for explicit md:grid-cols-3 only when you need fixed column counts per breakpoint.
-
Never use fixed pixel widths for layout. No w-[640px] on containers. Use fluid w-full + a max-w-* cap + horizontal auto margins: w-full max-w-5xl mx-auto px-4 sm:px-6 lg:px-8. Fixed sizes belong only on intrinsically-sized things (icons, avatars).
-
Dark mode = class strategy, controllable. In v4 add a custom variant so a root class toggles it (don't rely on prefers-color-scheme alone — users want a manual switch):
@custom-variant dark (&:where(.dark, .dark *));
Then bg-white dark:bg-bg text-gray-900 dark:text-gray-100. Toggle by adding/removing .dark on <html>. Define BOTH light and dark for every color-bearing element in the same edit — half-themed UI is the #1 dark-mode bug.
-
shadcn/ui integration: shadcn ships its semantic tokens (--background, --foreground, --primary, --border, --ring) as CSS vars wired into @theme inline. Theme the app by editing those vars under :root and .dark — do NOT hardcode colors on shadcn components. Run npx shadcn@latest init and let it write the vars; then customize values, not the component classes.
-
Extract repeated utility clusters into components, not @apply. If the same 6+ class string appears ≥3 times (e.g. a card shell), make a component (or a shadcn variant via cva). Reserve @apply for true global primitives only. Magic numbers and copy-pasted class walls are the thing this skill exists to prevent.
-
Container queries for component-level responsiveness. When a component must adapt to ITS container width (sidebar vs main), not the viewport, use @container on the parent and @sm: @md: on children. This is correct for reusable cards that live in differently-sized slots — viewport breakpoints can't see container width.