Loading...
Loading...
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.
npx skill4agent add ederheisler/agent-skills tailwind-v4-shadcn# 1. Install dependencies
bun add tailwindcss @tailwindcss/vite
bun add -D @types/node tw-animate-css
bunx shadcn@latest init
# 2. Delete v3 config if exists
rm tailwind.config.ts # v4 doesn't use this fileimport { 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') } }
}){
"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 basehsl()@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
/* ... map ALL CSS variables */
}bg-backgroundtext-primary@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>
)bunx shadcn@latest add dropdown-menureference/dark-mode.mdhsl():root.dark--bg: hsl(0 0% 100%);@theme inline"tailwind.config": ""tailwind.config.ts@tailwindcss/vite:root.dark@layer base.dark { @theme { } }hsl(var(--background))tailwind.config.ts@applydark:@apply@layer base@layer components@utility@layer basetailwindcss-animate# ✅ DO
bun add -D tw-animate-css
# Add to src/index.css:
@import "tailwindcss";
@import "tw-animate-css";
# ❌ DON'T
npm install tailwindcss-animate # v3 onlybg-primary@theme inline@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
/* ... map ALL CSS variables */
}templates/theme-provider.tsxmain.tsx.dark<html>@layer base/* ✅ 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.tsrm tailwind.config.tssrc/index.css@theme@theme inlinedata-mode="dark"@theme inline@theme inlinebg-primarybackground-color: oklch(...)@theme/* ✅ 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@layer base@layer components@apply@layer@utility@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@layer base@layer base/components/utilities@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);
}
}@layer base@import "tailwindcss";
:root {
--background: hsl(0 0% 100%);
}
body {
background-color: var(--background); /* No @layer needed */
}@layer base| 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 |
.bg-blue-500 {
background-color: #3b82f6; /* sRGB fallback */
background-color: oklch(0.6 0.24 264); /* Modern browsers */
}@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><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@tailwindcss/line-clamp@pluginrequire()@importbun add -D @tailwindcss/typography@import "tailwindcss";
@plugin "@tailwindcss/typography";<article class="prose dark:prose-invert">{{ content }}</article>bun add -D @tailwindcss/forms@import "tailwindcss";
@plugin "@tailwindcss/forms";<div className="@container">
<div className="@md:text-lg">Responds to container width</div>
</div>/* ❌ WRONG - v3 syntax */
@import "@tailwindcss/typography";
/* ✅ CORRECT - v4 syntax */
@plugin "@tailwindcss/typography";@tailwindcss/vitevite.config.tstailwindcss()components.json"config": ""tailwind.config.tssrc/index.css:root.darkhsl()@theme inline@layer basetemplates/cn()reference/migration-guide.mdtailwind.config.ts@theme inline@tailwindcss/line-clampline-clamp-*tailwindcss-animatetw-animate-cssrequire()@plugin@tailwindcss/upgrade<h1><h6>bun add -D @tailwindcss/typography@import "tailwindcss";
@plugin "@tailwindcss/typography";<article className="prose dark:prose-invert">
{/* All elements styled automatically */}
</article>@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// ✅ 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-importpostcss-advanced-variablestailwindcss/nesting@tailwindcss/postcssringring-3// 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>