tailwind-v4-shadcn
Original:🇺🇸 English
Translated
2 scriptsChecked / no sensitive code detected
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.
2.7kinstalls
Sourcejezweb/claude-skills
Added on
NPX Install
npx skill4agent add jezweb/claude-skills tailwind-v4-shadcnTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Tailwind v4 + shadcn/ui Production Stack
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 ✅
Quick Start (Follow This Exact Order)
bash
# 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:
typescript
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):
json
{
"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
css
/* 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 ). Use wrapper.
@layer basehsl()Step 2: Map Variables to Tailwind Utilities
css
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
/* ... map ALL CSS variables */
}Why: Generates utility classes (, ). Without this, utilities won't exist.
bg-backgroundtext-primaryStep 3: Apply Base Styles
css
@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
tsx
<div className="bg-background text-foreground">
{/* No dark: variants needed - theme switches automatically */}
</div>Dark Mode Setup
1. Create ThemeProvider (see )
templates/theme-provider.tsx2. Wrap App:
typescript
// 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:
bash
pnpm dlx shadcn@latest add dropdown-menuSee for ModeToggle component.
reference/dark-mode.mdCritical Rules
✅ Always Do:
- Wrap colors with in
hsl()/:root:.dark--bg: hsl(0 0% 100%); - Use to map all CSS variables
@theme inline - Set in components.json
"tailwind.config": "" - Delete if exists
tailwind.config.ts - Use plugin (NOT PostCSS)
@tailwindcss/vite
❌ Never Do:
- Put /
:rootinside.dark(causes cascade issues)@layer base - Use pattern (v4 doesn't support nested @theme)
.dark { @theme { } } - Double-wrap colors:
hsl(var(--background)) - Use for theme (v4 ignores it)
tailwind.config.ts - Use directive (deprecated in v4, see error #7)
@apply - Use variants for semantic colors (auto-handled)
dark: - Use with
@applyor@layer baseclasses (v4 breaking change - use@layer componentsinstead) | Source@utility - Wrap ANY styles in without understanding CSS layer ordering (see error #8) | Source
@layer base
Common Errors & Solutions
This skill prevents 8 documented errors.
1. ❌ tw-animate-css Import Error
Error: "Cannot find module 'tailwindcss-animate'"
Cause: shadcn/ui deprecated for v4.
tailwindcss-animateSolution:
bash
# ✅ 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 only2. ❌ Colors Not Working
Error: doesn't apply styles
bg-primaryCause: Missing mapping
@theme inlineSolution:
css
@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 class toggles on
.darkelement<html>
4. ❌ Duplicate @layer base
Error: "Duplicate @layer base" in console
Cause: shadcn init adds - don't add another
@layer baseSolution:
css
/* ✅ 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 (v3 legacy)
tailwind.config.tsSolution:
bash
rm tailwind.config.tsv4 configuration happens in using directive.
src/index.css@theme6. ❌ @theme inline Breaks Dark Mode in Multi-Theme Setups
Error: Dark mode doesn't switch when using with custom variants (e.g., )
Source: GitHub Discussion #18560
@theme inlinedata-mode="dark"Cause: 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 inlineWhy It Happens:
- inlines VALUES at build time:
@theme inline→bg-primarybackground-color: oklch(...) - Dark mode overrides change the CSS variables, but utilities already have baked-in values
- The CSS specificity chain breaks
Solution: Use (without inline) for multi-theme scenarios:
@themecss
/* ✅ 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:
- Single theme + dark mode toggle (like shadcn/ui default) ✅
- Referencing other CSS variables that don't change ✅
When NOT to use inline:
- Multi-theme systems (data-theme="blue" | "green" | etc.) ❌
- Dynamic theme switching beyond light/dark ❌
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."
7. ❌ @apply with @layer base/components (v4 Breaking Change)
Error:
Source: GitHub Discussion #17082
Cannot apply unknown utility class: custom-buttonCause: In v3, classes defined in and could be used with . In v4, this is a breaking architectural change.
@layer base@layer components@applyWhy It Happens: v4 doesn't "hijack" the native CSS at-rule anymore. Only classes defined with are available to .
@layer@utility@applyMigration:
css
/* ❌ 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 usage. This error is primarily for users migrating from v3.
@apply8. ❌ @layer base Styles Not Applying
Error: Styles defined in seem to be ignored
Source: GitHub Discussion #16002 | Discussion #18123
@layer baseCause: 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:
- v3: Tailwind intercepted and processed them specially
@layer base/components/utilities - v4: Uses native CSS layers - if you don't import layers in the right order, precedence breaks
- Styles ARE being applied, but utilities override them
Solution Option 1: Define layers explicitly:
css
@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 - define styles at root level:
@layer basecss
@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 unless you understand CSS layer ordering.
@layer baseQuick Reference
| Symptom | Cause | Fix |
|---|---|---|
| Missing | Add |
| Colors all black/white | Double | Use |
| Dark mode not switching | Missing ThemeProvider | Wrap app in |
| Build fails | | Delete file |
| Animation errors | Using | Install |
What's New in Tailwind v4
OKLCH Color Space (December 2024)
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:
- Perceptual consistency: HSL's "50% lightness" is visually inconsistent across hues (yellow appears much brighter than blue at same lightness)
- Better gradients: Smooth transitions without muddy middle colors
- Wider gamut: Supports colors beyond sRGB on modern displays
- More vibrant colors: Eye-catching, saturated colors previously limited by sRGB
Browser Support (January 2026):
- Chrome 111+, Firefox 113+, Safari 15.4+, Edge 111+
- Global coverage: 93.1%
Automatic Fallbacks: Tailwind generates sRGB fallbacks for older browsers:
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:
css
@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.
Built-in Features (No Plugin Needed)
Container Queries (built-in as of v4.0):
tsx
<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):
tsx
<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:
- - Built-in now
@tailwindcss/container-queries - - Built-in since v3.3
@tailwindcss/line-clamp
Tailwind v4 Plugins
Use directive (NOT or ):
@pluginrequire()@importTypography (for Markdown/CMS content):
bash
pnpm add -D @tailwindcss/typographycss
@import "tailwindcss";
@plugin "@tailwindcss/typography";html
<article class="prose dark:prose-invert">{{ content }}</article>Forms (cross-browser form styling):
bash
pnpm add -D @tailwindcss/formscss
@import "tailwindcss";
@plugin "@tailwindcss/forms";Container Queries (built-in, no plugin needed):
tsx
<div className="@container">
<div className="@md:text-lg">Responds to container width</div>
</div>Common Plugin Errors:
css
/* ❌ WRONG - v3 syntax */
@import "@tailwindcss/typography";
/* ✅ CORRECT - v4 syntax */
@plugin "@tailwindcss/typography";Setup Checklist
- installed (NOT postcss)
@tailwindcss/vite - uses
vite.config.tsplugintailwindcss() - has
components.json"config": "" - NO exists
tailwind.config.ts - follows 4-step pattern:
src/index.css- /
:rootat root level (not in @layer).dark - Colors wrapped with
hsl() - maps all variables
@theme inline - uses unwrapped variables
@layer base
-
- ThemeProvider wraps app
- Theme toggle works
File Templates
Available in directory:
templates/- 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 - utility
cn()
Migration from v3
See for complete guide.
reference/migration-guide.mdKey Changes:
- Delete
tailwind.config.ts - Move theme to CSS with
@theme inline - Replace (now built-in:
@tailwindcss/line-clamp)line-clamp-* - Replace with
tailwindcss-animatetw-animate-css - Update plugins: →
require()@plugin
Additional Migration Gotchas
Automated Migration Tool May Fail
Warning: The utility often fails to migrate configurations.
Source: Community Reports | GitHub Discussion #16642
@tailwindcss/upgradeCommon failures:
- Typography plugin configurations
- Complex theme extensions
- Custom plugin setups
Recommendation: Don't rely on automated migration. Follow manual steps in the migration guide instead.
Default Element Styles Removed
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:
- All headings (through
<h1>) render at same size<h6> - Lists lose default padding
- Visual regressions in existing projects
Solutions:
Option 1: Use @tailwindcss/typography for content pages:
bash
pnpm add -D @tailwindcss/typographycss
@import "tailwindcss";
@plugin "@tailwindcss/typography";tsx
<article className="prose dark:prose-invert">
{/* All elements styled automatically */}
</article>Option 2: Add custom base styles:
css
@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; }
}PostCSS Setup Complexity
Recommendation: Use plugin for Vite projects instead of PostCSS.
Source: Medium: Migration Problems | GitHub Discussion #15764
@tailwindcss/viteWhy Vite Plugin is Better:
typescript
// ✅ 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:
- Error: "It looks like you're trying to use tailwindcss directly as a PostCSS plugin"
- Multiple PostCSS plugins required: ,
postcss-import,postcss-advanced-variablestailwindcss/nesting - v4 PostCSS plugin is separate package:
@tailwindcss/postcss
Official Guidance: The Vite plugin is recommended for Vite projects. PostCSS is for legacy setups or non-Vite environments.
Visual Changes
Ring Width Default: Changed from 3px to 1px
Source: Medium: Migration Guide
- class is now thinner
ring - Use to match v3 appearance
ring-3
tsx
// 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>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-20
Skill Version: 3.0.0
Tailwind v4: 4.1.18 (Latest)
Production: WordPress Auditor (https://wordpress-auditor.webfonts.workers.dev)
Changelog:
- v3.0.0 (2026-01-20): Major research update - added 3 TIER 1 errors (#6-8), expanded migration guide with community findings (TIER 2), added OKLCH color space section, PostCSS complexity warnings, and migration tool limitations
- v2.0.1 (2026-01-03): Production verification
- v2.0.0: Initial release with 5 documented errors