**Production-tested**: WordPress Auditor ([https://wordpress-auditor.webfonts.workers.dev](https://wordpress-auditor.webfonts.workers.dev)) **Last Updated**: 2026-01-20 **Versions**: tailwindcss@4.1.18, @tailwindcss/vite@4.1.18 **Status**: Production Ready ✅ * * * ```
# 1. Install dependencies pnpm add tailwindcss @tailwindcss/vite pnpm add -D @types/node tw-animate-css pnpm dlx shadcn@latest init # 2. Delete v3 config if exists rm tailwind.config.ts # v4 doesn't use this file `**vite.config.ts**:` import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' import path from 'path' export default defineConfig({ plugins: [react(), tailwindcss()], resolve: { alias: { '@': path.resolve(__dirname, './src') } } }) `**components.json** (CRITICAL):` { "tailwind": { "config": "", // ← Empty for v4 "css": "src/index.css", "baseColor": "slate", "cssVariables": true } }
/* src/index.css */ @import "tailwindcss"; @import "tw-animate-css"; /* Required for shadcn/ui animations */ :root { --background: hsl(0 0% 100%); /* ← hsl() wrapper required */ --foreground: hsl(222.2 84% 4.9%); --primary: hsl(221.2 83.2% 53.3%); /* ... all light mode colors */ } .dark { --background: hsl(222.2 84% 4.9%); --foreground: hsl(210 40% 98%); --primary: hsl(217.2 91.2% 59.8%); /* ... all dark mode colors */ }
@layer base). Use hsl() wrapper.@theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --color-primary: var(--primary); /* ... map ALL CSS variables */ }
bg-background, text-primary). Without this, utilities won't exist.@layer base { body { background-color: var(--background); /* NO hsl() wrapper here */ color: var(--foreground); } }
hsl(var(--background)).<div className="bg-background text-foreground"> {/* No dark: variants needed - theme switches automatically */} </div>
templates/theme-provider.tsx)// src/main.tsx import { ThemeProvider } from '@/components/theme-provider' ReactDOM.createRoot(document.getElementById('root')!).render( <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> <App /> </ThemeProvider> ) `**3\. Add Theme Toggle**:` pnpm dlx shadcn@latest add dropdown-menu
reference/dark-mode.md for ModeToggle component.hsl() in :root/.dark: --bg: hsl(0 0% 100%);@theme inline to map all CSS variables"tailwind.config": "" in components.jsontailwind.config.ts if exists@tailwindcss/vite plugin (NOT PostCSS):root/.dark inside @layer base (causes cascade issues).dark { @theme { } } pattern (v4 doesn't support nested @theme)hsl(var(--background))tailwind.config.ts for theme (v4 ignores it)@apply directive (deprecated in v4, see error #7)dark: variants for semantic colors (auto-handled)@apply with @layer base or @layer components classes (v4 breaking change - use @utility instead) | Source@layer base without understanding CSS layer ordering (see error #8) | Sourcetailwindcss-animate for v4.# ✅ DO pnpm add -D tw-animate-css # Add to src/index.css: @import "tailwindcss"; @import "tw-animate-css"; # ❌ DON'T npm install tailwindcss-animate # v3 only
bg-primary doesn't apply styles@theme inline mapping@theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --color-primary: var(--primary); /* ... map ALL CSS variables */ }
templates/theme-provider.tsx)main.tsx.dark class toggles on <html> element@layer base - don't add another/* ✅ Correct - single @layer base */ @import "tailwindcss"; :root { --background: hsl(0 0% 100%); } @theme inline { --color-background: var(--background); } @layer base { body { background-color: var(--background); } }
tailwind.config.ts (v3 legacy)rm tailwind.config.tssrc/index.css using @theme directive.@theme inline with custom variants (e.g., data-mode="dark") Source: GitHub Discussion #18560@theme inline bakes variable VALUES into utilities at build time. When dark mode changes the underlying CSS variables, utilities don't update because they reference hardcoded values, not variables.@theme inline inlines VALUES at build time: bg-primary → background-color: oklch(...)@theme (without inline) for multi-theme scenarios:/* ✅ CORRECT - Use @theme without inline */ @custom-variant dark (&:where([data-mode=dark], [data-mode=dark] *)); @theme { --color-text-primary: var(--color-slate-900); --color-bg-primary: var(--color-white); } @layer theme { [data-mode="dark"] { --color-text-primary: var(--color-white); --color-bg-primary: var(--color-slate-900); } }
"It's more idiomatic in v4 for the actual generated CSS to reference your theme variables. I would personally only use inline when things don't work without it."
Cannot apply unknown utility class: custom-button Source: GitHub Discussion #17082@layer base and @layer components could be used with @apply. In v4, this is a breaking architectural change.@layer at-rule anymore. Only classes defined with @utility are available to @apply./* ❌ v3 pattern (worked) */ @layer components { .custom-button { @apply px-4 py-2 bg-blue-500; } } /* ✅ v4 pattern (required) */ @utility custom-button { @apply px-4 py-2 bg-blue-500; } /* OR use native CSS */ @layer base { .custom-button { padding: 1rem 0.5rem; background-color: theme(colors.blue.500); } }
@apply usage. This error is primarily for users migrating from v3.@layer base seem to be ignored Source: GitHub Discussion #16002 | Discussion #18123@layer base/components/utilities and processed them specially@import "tailwindcss/theme.css" layer(theme); @import "tailwindcss/base.css" layer(base); @import "tailwindcss/components.css" layer(components); @import "tailwindcss/utilities.css" layer(utilities); @layer base { body { background-color: var(--background); } } `**Solution Option 2** (Recommended): Don't use `@layer base` - define styles at root level:` @import "tailwindcss"; :root { --background: hsl(0 0% 100%); } body { background-color: var(--background); /* No @layer needed */ }
@layer base unless you understand CSS layer ordering.bg-primary doesn't work@theme inline@theme inline blockhsl() wrappingvar(--color) not hsl(var(--color))<ThemeProvider>tailwind.config.ts existstailwindcss-animatetw-animate-css.bg-blue-500 { background-color: #3b82f6; /* sRGB fallback */ background-color: oklch(0.6 0.24 264); /* Modern browsers */ } `**Custom Colors**: When defining custom colors, OKLCH is now preferred:` @theme { /* Modern approach (preferred) */ --color-brand: oklch(0.7 0.15 250); /* Legacy approach (still works) */ --color-brand: hsl(240 80% 60%); }
<div className="@container"> <div className="@md:text-lg @lg:grid-cols-2"> Content responds to container width, not viewport </div> </div> `**Line Clamp** (built-in as of v3.3):` <p className="line-clamp-3">Truncate to 3 lines with ellipsis...</p> <p className="line-clamp-[8]">Arbitrary values supported</p> <p className="line-clamp-(--teaser-lines)">CSS variable support</p>
@tailwindcss/container-queries - Built-in now@tailwindcss/line-clamp - Built-in since v3.3@plugin directive (NOT require() or @import):pnpm add -D @tailwindcss/typography@import "tailwindcss"; @plugin "@tailwindcss/typography";
<article>{{ content }}</article>pnpm add -D @tailwindcss/forms@import "tailwindcss"; @plugin "@tailwindcss/forms"; `**Container Queries** (built-in, no plugin needed):` <div className="@container"> <div className="@md:text-lg">Responds to container width</div> </div> `**Common Plugin Errors**:` /* ❌ WRONG - v3 syntax */ @import "@tailwindcss/typography"; /* ✅ CORRECT - v4 syntax */ @plugin "@tailwindcss/typography";
@tailwindcss/vite installed (NOT postcss)vite.config.ts uses tailwindcss() plugincomponents.json has "config": ""tailwind.config.ts existssrc/index.css follows 4-step pattern:
- :root/.dark at root level (not in @layer)
- Colors wrapped with hsl()
- @theme inline maps all variables
- @layer base uses unwrapped variablestemplates/ directory:cn() utilityreference/migration-guide.md for complete guide.tailwind.config.ts@theme inline@tailwindcss/line-clamp (now built-in: line-clamp-*)tailwindcss-animate with tw-animate-cssrequire() → @plugin@tailwindcss/upgrade utility often fails to migrate configurations. Source: Community Reports | GitHub Discussion #16642<h1> through <h6>) render at same sizepnpm add -D @tailwindcss/typography@import "tailwindcss"; @plugin "@tailwindcss/typography";
<article className="prose dark:prose-invert"> {/* All elements styled automatically */} </article> `**Option 2: Add custom base styles**:` @layer base { h1 { @apply text-4xl font-bold mb-4; } h2 { @apply text-3xl font-bold mb-3; } h3 { @apply text-2xl font-bold mb-2; } ul { @apply list-disc pl-6 mb-4; } ol { @apply list-decimal pl-6 mb-4; } }
@tailwindcss/vite plugin for Vite projects instead of PostCSS. Source: Medium: Migration Problems | GitHub Discussion #15764// ✅ Vite Plugin - One line, no PostCSS config import tailwindcss from '@tailwindcss/vite' export default defineConfig({ plugins: [react(), tailwindcss()], }) // ❌ PostCSS - Multiple steps, plugin compatibility issues // 1. Install @tailwindcss/postcss // 2. Configure postcss.config.js // 3. Manage plugin order // 4. Debug plugin conflicts
postcss-import, postcss-advanced-variables, tailwindcss/nesting@tailwindcss/postcssring class is now thinnerring-3 to match v3 appearance// v3: 3px ring <button className="ring">Button</button> // v4: 1px ring (thinner) <button className="ring">Button</button> // Match v3 appearance <button className="ring-3">Button</button>