tessl install github:jezweb/claude-skills --skill tailwind-v4-shadcngithub.com/jezweb/claude-skills
Set up Tailwind v4 with shadcn/ui using @theme inline pattern and CSS variable architecture. Four-step pattern: CSS variables, Tailwind mapping, base styles, automatic dark mode. Prevents 8 documented errors. Use when initializing React projects with Tailwind v4, or fixing colors not working, tw-animate-css errors, @theme inline dark mode conflicts, @apply breaking, v3 migration issues.
Review Score
90%
Validation Score
12/16
Implementation Score
85%
Activation Score
100%
Production-tested: WordPress Auditor (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 filevite.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-menuSee 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) | SourceThis skill prevents 8 documented 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 onlyError: 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.tsv4 configuration happens in src/index.css using @theme directive.
Error: Dark mode doesn't switch when using @theme inline with custom variants (e.g., data-mode="dark")
Source: GitHub Discussion #18560
Cause: @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.
Why It Happens:
@theme inline inlines VALUES at build time: bg-primary → background-color: oklch(...)Solution: Use @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);
}
}When to use inline:
When NOT to use inline:
Maintainer Guidance (Adam Wathan):
"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."
Error: Cannot apply unknown utility class: custom-button
Source: GitHub Discussion #17082
Cause: In v3, classes defined in @layer base and @layer components could be used with @apply. In v4, this is a breaking architectural change.
Why It Happens: v4 doesn't "hijack" the native CSS @layer at-rule anymore. Only classes defined with @utility are available to @apply.
Migration:
/* ❌ 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);
}
}Note: This skill already discourages @apply usage. This error is primarily for users migrating from v3.
Error: Styles defined in @layer base seem to be ignored
Source: GitHub Discussion #16002 | Discussion #18123
Cause: v4 uses native CSS layers. Base styles CAN be overridden by utility layers due to CSS cascade if layers aren't explicitly ordered.
Why It Happens:
@layer base/components/utilities and processed them speciallySolution Option 1: Define layers explicitly:
@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 */
}Applies to: ALL base styles, not just color variables. Avoid wrapping ANY styles in @layer base unless you understand CSS layer ordering.
| 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 |
Tailwind v4.0 replaced the entire default color palette with OKLCH, a perceptually uniform color space. Source: Tailwind v4.0 Release | OKLCH Migration Guide
Why OKLCH:
Browser Support (January 2026):
Automatic Fallbacks: Tailwind generates sRGB fallbacks for older browsers:
.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%);
}Migration: No breaking changes - Tailwind generates fallbacks automatically. For new projects, use OKLCH-aware tooling for custom colors.
Container Queries (built-in as of v4.0):
<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>Removed Plugins:
@tailwindcss/container-queries - Built-in now@tailwindcss/line-clamp - Built-in since v3.3Use @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() → @pluginWarning: The @tailwindcss/upgrade utility often fails to migrate configurations.
Source: Community Reports | GitHub Discussion #16642
Common failures:
Recommendation: Don't rely on automated migration. Follow manual steps in the migration guide instead.
Tailwind v4 takes a more minimal approach to Preflight, removing default styles for headings, lists, and buttons. Source: GitHub Discussion #16517 | Medium: Migration Problems
Impact:
<h1> through <h6>) render at same sizeSolutions:
Option 1: Use @tailwindcss/typography for content pages:
pnpm 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; }
}Recommendation: Use @tailwindcss/vite plugin for Vite projects instead of PostCSS.
Source: Medium: Migration Problems | GitHub Discussion #15764
Why Vite Plugin is Better:
// ✅ 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 conflictsPostCSS Problems Reported:
postcss-import, postcss-advanced-variables, tailwindcss/nesting@tailwindcss/postcssOfficial Guidance: The Vite plugin is recommended for Vite projects. PostCSS is for legacy setups or non-Vite environments.
Ring Width Default: Changed from 3px to 1px Source: Medium: Migration Guide
ring 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>Last Updated: 2026-01-20 Skill Version: 3.0.0 Tailwind v4: 4.1.18 (Latest) Production: WordPress Auditor (https://wordpress-auditor.webfonts.workers.dev)
Changelog: