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. Use when initializing React projects with Tailwind v4, or fixing colors not working, tw-animate-css errors, v3 migration.
Installation
Details
Usage
After installing, this skill will be available to your AI coding assistant.
Verify installation:
skills listSkill Instructions
name: 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.
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.
Tailwind v4 + shadcn/ui Production Stack
Production-tested: WordPress Auditor (https://wordpress-auditor.webfonts.workers.dev) Last Updated: 2026-01-03 Versions: tailwindcss@4.1.18, @tailwindcss/vite@4.1.18 Status: Production Ready ✅
Quick Start (Follow This Exact Order)
# 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
}
}
The Four-Step Architecture (MANDATORY)
Skipping steps will break your theme. Follow exactly:
Step 1: Define CSS Variables at Root
/* 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.
Step 2: Map Variables to Tailwind Utilities
@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.
Step 3: Apply Base Styles
@layer base {
body {
background-color: var(--background); /* NO hsl() wrapper here */
color: var(--foreground);
}
}
Critical: Reference variables directly. Never double-wrap: hsl(var(--background)).
Step 4: Result - Automatic Dark Mode
<div className="bg-background text-foreground">
{/* No dark: variants needed - theme switches automatically */}
</div>
Dark Mode Setup
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.
Critical Rules
✅ Always Do:
- Wrap colors with
hsl()in:root/.dark:--bg: hsl(0 0% 100%); - Use
@theme inlineto map all CSS variables - Set
"tailwind.config": ""in components.json - Delete
tailwind.config.tsif exists - Use
@tailwindcss/viteplugin (NOT PostCSS)
❌ Never Do:
- Put
:root/.darkinside@layer base - Use
.dark { @theme { } }pattern (v4 doesn't support nested @theme) - Double-wrap colors:
hsl(var(--background)) - Use
tailwind.config.tsfor theme (v4 ignores it) - Use
@applydirective (deprecated in v4) - Use
dark:variants for semantic colors (auto-handled)
Common Errors & Solutions
This skill prevents 5 common errors.
1. ❌ tw-animate-css Import Error
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
2. ❌ Colors Not Working
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 */
}
3. ❌ Dark Mode Not Switching
Error: Theme stays light/dark
Cause: Missing ThemeProvider
Solution:
- Create ThemeProvider (see
templates/theme-provider.tsx) - Wrap app in
main.tsx - Verify
.darkclass toggles on<html>element
4. ❌ Duplicate @layer base
Error: "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); } }
5. ❌ Build Fails with tailwind.config.ts
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.
Quick Reference
| 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 Plugins
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";
Setup Checklist
-
@tailwindcss/viteinstalled (NOT postcss) -
vite.config.tsusestailwindcss()plugin -
components.jsonhas"config": "" - NO
tailwind.config.tsexists -
src/index.cssfollows 4-step pattern:-
:root/.darkat root level (not in @layer) - Colors wrapped with
hsl() -
@theme inlinemaps all variables -
@layer baseuses unwrapped variables
-
- ThemeProvider wraps app
- Theme toggle works
File Templates
Available in templates/ directory:
- index.css - Complete CSS with all color variables
- components.json - shadcn/ui v4 config
- vite.config.ts - Vite + Tailwind plugin
- theme-provider.tsx - Dark mode provider
- utils.ts -
cn()utility
Migration from v3
See reference/migration-guide.md for complete guide.
Key Changes:
- Delete
tailwind.config.ts - Move theme to CSS with
@theme inline - Replace
@tailwindcss/line-clamp(now built-in:line-clamp-*) - Replace
tailwindcss-animatewithtw-animate-css - Update plugins:
require()→@plugin
Reference Documentation
- architecture.md - Deep dive into 4-step pattern
- dark-mode.md - Complete dark mode implementation
- common-gotchas.md - Troubleshooting guide
- migration-guide.md - v3 → v4 migration
Official Documentation
- shadcn/ui Vite Setup: https://ui.shadcn.com/docs/installation/vite
- shadcn/ui Tailwind v4: https://ui.shadcn.com/docs/tailwind-v4
- Tailwind v4 Docs: https://tailwindcss.com/docs
Last Updated: 2026-01-03 Skill Version: 2.0.1 Tailwind v4: 4.1.18 (Latest) Production: WordPress Auditor (https://wordpress-auditor.webfonts.workers.dev)
More by jezweb
View allSelf-hosted auth for TypeScript/Cloudflare Workers with social auth, 2FA, passkeys, organizations, RBAC, and 15+ plugins. Requires Drizzle ORM or Kysely for D1 (no direct adapter). Self-hosted alternative to Clerk/Auth.js. Use when: self-hosting auth on D1, building OAuth provider, multi-tenant SaaS, or troubleshooting D1 adapter errors, session caching, rate limits.
/review-skill - Skill Audit Command: Comprehensive skill documentation audit with automated checks and manual review phases.
Build rich text editors with Tiptap - headless editor framework with React and Tailwind v4. Covers SSR-safe setup, image uploads, prose styling, and collaborative editing. Use when creating blog editors, comment systems, or Notion-like apps, or troubleshooting SSR hydration errors, typography issues, or image upload problems.
Run LLMs and AI models on Cloudflare's GPU network with Workers AI. Includes Llama 4, Gemma 3, Mistral 3.1, Flux images, BGE embeddings, streaming, and AI Gateway. Handles 2025 breaking changes. Use when: implementing LLM inference, images, RAG, or troubleshooting AI_ERROR, rate limits, max_tokens, BGE pooling.
