Set up Tailwind v4 with shadcn/ui using @theme inline pattern and CSS variable architecture. Four-step mandatory pattern: define CSS variables at root, map to Tailwind utilities, apply base styles, get automatic dark mode. Use when: initializing React projects with Tailwind v4, setting up shadcn/ui dark mode, or fixing colors not working, theme not applying, CSS variables broken, tw-animate-css errors, or migrating from v3.
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
README.mdreferences/architecture.mdreferences/common-gotchas.mdreferences/dark-mode.mdreferences/migration-guide.mdrules/tailwind-v4-shadcn.mdtemplates/components.jsontemplates/index.csstemplates/theme-provider.tsxtemplates/tsconfig.app.jsontemplates/utils.tstemplates/vite.config.tsname: tailwind-v4-shadcn description: | Set up Tailwind v4 with shadcn/ui using @theme inline pattern and CSS variable architecture. Four-step mandatory pattern: define CSS variables at root, map to Tailwind utilities, apply base styles, get automatic dark mode.
Production-tested: WordPress Auditor (https://wordpress-auditor.webfonts.workers.dev) Last Updated: 2025-11-28 Versions: tailwindcss@4.1.17, @tailwindcss/vite@4.1.17 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
}
}
Skipping steps will break your theme. Follow exactly:
/* 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 */
}
Critical: Define at root level (NOT inside @layer base). Use hsl() wrapper.
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
/* ... map ALL CSS variables */
}
Why: Generates utility classes (bg-background, text-primary). Without this, utilities won't exist.
@layer base {
body {
background-color: var(--background); /* NO hsl() wrapper here */
color: var(--foreground);
}
}
Critical: Reference variables directly. Never double-wrap: hsl(var(--background)).
<div className="bg-background text-foreground">
{/* No dark: variants needed - theme switches automatically */}
</div>
1. Create ThemeProvider (see templates/theme-provider.tsx)
2. Wrap App:
// 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
See 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.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)dark: variants for semantic colors (auto-handled)This skill prevents 5 common errors.
Error: "Cannot find module 'tailwindcss-animate'"
Cause: shadcn/ui deprecated tailwindcss-animate for v4.
Solution:
# ✅ 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
Error: bg-primary doesn't apply styles
Cause: Missing @theme inline mapping
Solution:
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
/* ... map ALL CSS variables */
}
Error: Theme stays light/dark
Cause: Missing ThemeProvider
Solution:
templates/theme-provider.tsx)main.tsx.dark class toggles on <html> elementError: "Duplicate @layer base" in console
Cause: shadcn init adds @layer base - don't add another
Solution:
/* ✅ 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); } }
Error: "Unexpected config file"
Cause: v4 doesn't use tailwind.config.ts (v3 legacy)
Solution:
rm tailwind.config.ts
v4 configuration happens in src/index.css using @theme directive.
| Symptom | Cause | Fix |
|---|---|---|
bg-primary doesn't work | Missing @theme inline | Add @theme inline block |
| Colors all black/white | Double hsl() wrapping | Use var(--color) not hsl(var(--color)) |
| Dark mode not switching | Missing ThemeProvider | Wrap app in <ThemeProvider> |
| Build fails | tailwind.config.ts exists | Delete file |
| Animation errors | Using tailwindcss-animate | Install tw-animate-css |
Use @plugin directive (NOT require() or @import):
Typography (for Markdown/CMS content):
pnpm add -D @tailwindcss/typography
@import "tailwindcss";
@plugin "@tailwindcss/typography";
<article class="prose dark:prose-invert">{{ content }}</article>
Forms (cross-browser form styling):
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)hsl()@theme inline maps all variables@layer base uses unwrapped variablesAvailable in templates/ directory:
cn() utilitySee reference/migration-guide.md for complete guide.
Key Changes:
tailwind.config.ts@theme inline@tailwindcss/line-clamp (now built-in: line-clamp-*)tailwindcss-animate with tw-animate-cssrequire() → @pluginLast Updated: 2025-11-28 Skill Version: 2.0.0 Tailwind v4: 4.1.17 (Latest) Production: WordPress Auditor (https://wordpress-auditor.webfonts.workers.dev)