SuperAPI AI Directory

shadcn-errors-tailwind-v3-v4-migration

Frontend & websites

View repository

Updated 10-15 22:18

About this skill

shadcn ui Errors: Tailwind v3 to v4 Migration

Tailwind CSS v4 (Jan 2025) rewrote the configuration model, the color format, the animation plugin, the dark mode trigger, the ring utility default, and the PostCSS integration. shadcn ui projects bootstrapped before v4 cannot be upgraded by npm install alone: the CSS file, the config file, the PostCSS file, and the package.json plugin list must all change in lockstep, or the project enters a half-migrated state where some classes resolve and others fail silently. This is the single largest hallucination risk in AI-generated shadcn code, because training data spans both generations and the v3 patterns look syntactically valid when pasted into a v4 project.

This skill gives a deterministic migration runbook and the detection rules to identify which generation a project is on before changing any code.

Quick Reference

Major shifts table (v3 -> v4)

Concept v3 (pre-2025) v4 (evergreen-2026)
Config location tailwind.config.js (JS) @theme inline block in CSS
CSS import @tailwind base; @tailwind components; @tailwind utilities; @import "tailwindcss";
Color format --background: 0 0% 100% (HSL space-separated) --background: oklch(1 0 0)
Color reference bg-[hsl(var(--background))] or tailwind.config.js color extension bg-background (direct, via @theme inline)
Animation plugin tailwindcss-animate (npm package, in plugins) tw-animate-css (CSS @import, no plugin entry)
Dark mode trigger darkMode: ['class'] in tailwind.config.js @custom-variant dark (&:is(.dark *)); in CSS
Ring default width ring = 3px, ring-2 = 2px ring = 1px, ring-3 = 3px
PostCSS plugin tailwindcss: {} '@tailwindcss/postcss': {}
Vite plugin none required (via PostCSS) @tailwindcss/vite preferred
@tailwind directives required REMOVED, do not use
forwardRef in components required pattern REMOVED, plain function declarations

Detection rules (read these BEFORE touching code)

Signal Conclusion
tailwind.config.js or tailwind.config.ts exists at project root Project was bootstrapped as v3
globals.css contains @tailwind base; Project is v3
globals.css contains @import "tailwindcss"; Project is v4
globals.css contains @theme inline Project is v4
globals.css contains --background: 0 0% 100% (no oklch, no hsl()) Token block is v3
globals.css contains --background: oklch(...) Token block is v4
package.json lists tailwindcss-animate Plugin is v3-era
package.json lists tw-animate-css Plugin is v4-era
postcss.config.js lists tailwindcss plugin PostCSS is v3-style
postcss.config.js lists '@tailwindcss/postcss' plugin PostCSS is v4-style
components/ui/*.tsx uses React.forwardRef Components are v3-era
components/ui/*.tsx uses plain function Component({...}) with data-slot Components are v4-era

ALWAYS run the detection scan first. NEVER edit one signal in isolation: a project where globals.css is v4 but tailwind.config.js still exists is a half-migrated project and produces unpredictable builds.

Migration runbook (deterministic order)

ALWAYS execute these steps in this exact order. Each step depends on the previous.

  1. Branch and snapshot : git checkout -b tailwind-v4-migration. NEVER migrate on main.
  2. Verify Node version : node --version must be >= 20. The v4 codemod requires Node 20+.
  3. Run the official codemod : npx @tailwindcss/upgrade. This rewrites dependencies, the config file, and template files automatically.
  4. Update dependencies to latest shadcn-compatible versions : pnpm up "@radix-ui/*" lucide-react recharts tailwind-merge clsx --latest.
  5. Replace the animate plugin : remove tailwindcss-animate from package.json dependencies. Add tw-animate-css. Replace @plugin 'tailwindcss-animate'; (if present) with @import "tw-animate-css"; at the top of globals.css.
  6. Verify token format : every CSS variable in :root and .dark must use oklch(...), hsl(...), or a named color, NEVER raw space-separated HSL components.
  7. Verify @theme inline block : globals.css must contain @theme inline { --color-background: var(--background); ... } so semantic class names like bg-background resolve.
  8. Verify dark mode wiring : globals.css must contain @custom-variant dark (&:is(.dark *));. The darkMode key in tailwind.config.js is no longer read. If tailwind.config.js still exists, delete it OR keep it ONLY if the project intentionally retains v3-compat mode (rare).
  9. Verify ring utility usage : grep for ring-2 in the codebase. v3 components used ring-2 for the focus ring. In v4 the visual equivalent is ring-3 (because ring itself dropped from 3px to 1px). Audit shadcn components that were copied pre-v4 (Button, Input, Select, Tabs, Switch).
  10. Re-run shadcn-add for any pre-v4 components : pnpm dlx shadcn@latest add button input select --overwrite regenerates the components in v4 form (with data-slot, without forwardRef, with v4-correct ring width). NEVER hand-edit the upgrade for a component when re-running the CLI will do it deterministically.
  11. Update PostCSS : postcss.config.js must list '@tailwindcss/postcss': {} instead of tailwindcss: {}. Remove postcss-import and autoprefixer entries (v4 handles imports and prefixing internally).
  12. Update Vite (if applicable) : install @tailwindcss/vite and add tailwindcss() to the vite.config.ts plugins array. This is now the preferred integration for Vite projects.
  13. Build and verify : run pnpm build. The build must complete with zero "unknown at-rule" warnings. Open the app, toggle dark mode, focus a Button, and confirm the ring appears at the expected thickness.
  14. Visual regression sweep : open every page. v3 -> v4 frequently regresses cursor on Button (v4 ships cursor: default on <button>; shadcn does NOT add cursor-pointer), ring thickness on focused inputs, accordion expand/collapse animation timing, and chart fill colors (when chartConfig still wraps hsl(var(--chart-1)) instead of var(--chart-1)).

NEVER stop at step 3. The codemod handles most of the rewrite but does not always migrate tailwindcss-animate to tw-animate-css, does not always rewrite ring-2 usages, and does not re-run the shadcn CLI to refresh component files. Steps 5 through 13 are manual.

Per-file changes

globals.css

The file flips from @tailwind directives + @layer base token block to @import "tailwindcss" + raw token block + @theme inline mapping + @custom-variant dark + @import "tw-animate-css". See examples.md for the full before/after.

tailwind.config.js

In v4 the JS config is OPTIONAL and not auto-detected. The preferred path for shadcn evergreen-2026 is: DELETE tailwind.config.js and move all theming into globals.css. If the project has app-specific extensions (custom fonts, custom keyframes that are not animation-related) the v4 escape hatch is @config "../../tailwind.config.js"; at the top of globals.css, but for stock shadcn projects this is not needed.

postcss.config.js

Three changes: rename the plugin key from tailwindcss to '@tailwindcss/postcss', remove postcss-import (v4 handles it), and remove autoprefixer (v4 handles vendor prefixing). The whole file becomes a single-plugin export. See examples.md.

package.json

Remove tailwindcss v3 entry. Add tailwindcss v4 + @tailwindcss/postcss (or @tailwindcss/vite for Vite projects). Remove tailwindcss-animate. Add tw-animate-css. Remove autoprefixer and postcss-import if they were standalone. The codemod from step 3 typically handles dependency bumps, but the animate-plugin swap is manual.

Verification checklist

After completing the runbook, ALL of these must be true:

  • node --version reports >= 20
  • tailwind.config.js is either deleted OR referenced explicitly via @config in CSS
  • globals.css starts with @import "tailwindcss";
  • globals.css contains exactly one @theme inline block
  • globals.css contains @custom-variant dark (&:is(.dark *));
  • globals.css imports tw-animate-css (NOT references tailwindcss-animate)
  • All CSS variables use oklch(...), hsl(...), or named colors; NO raw 0 0% 100%
  • package.json has NO tailwindcss-animate entry
  • package.json has tw-animate-css AND tailwindcss v4
  • postcss.config.js lists '@tailwindcss/postcss', not tailwindcss
  • Vite projects use @tailwindcss/vite plugin
  • No file mixes bg-[hsl(var(--background))] with bg-background (the former is v3 syntax)
  • No file mixes ring-2 with components that were re-pulled in v4 form
  • Dark mode toggle adds class="dark" on <html> AND the page restyles
  • Build emits zero "unknown at-rule" warnings

If ANY checkbox fails, the project is half-migrated. Re-enter the runbook at the appropriate step.

Decision Trees

"My colors broke after upgrade"

Did colors break?
├── Yes : open globals.css
│   ├── Tokens use `0 0% 100%` (no oklch, no hsl wrapper) AND file imports tailwindcss v4
│   │   └── This is the canonical v3-token-in-v4-file bug
│   │       Fix : wrap each token with `oklch(...)` or `hsl(...)`, e.g. `--background: oklch(1 0 0);`
│   ├── Tokens use `oklch(...)` AND tailwind.config.js still extends colors with `hsl(var(--background))`
│   │   └── Two color sources fighting; the JS config still expects HSL
│   │       Fix : delete tailwind.config.js color extension, add `@theme inline { --color-background: var(--background); }` in CSS
│   └── Tokens look correct, but className uses `bg-[hsl(var(--background))]`
│       └── This is v3 arbitrary-class syntax
│           Fix : replace with `bg-background`
└── No : skip

"My animations stopped working"

Is package.json still listing tailwindcss-animate?
├── Yes : v4 removed support for this plugin
│   Fix :
│   1. `pnpm remove tailwindcss-animate`
│   2. `pnpm add tw-animate-css`
│   3. In globals.css, add `@import "tw-animate-css";` near the top
│   4. Remove any `@plugin 'tailwindcss-animate';` line from CSS
└── No : check the build log for "unknown at-rule" warnings
    └── @plugin syntax may still reference the old plugin name

"My dark toggle stopped working"

Open globals.css
├── No `@custom-variant dark` line found
│   └── v3 relied on `darkMode: ['class']` in JS config; v4 needs CSS variant
│       Fix : add `@custom-variant dark (&:is(.dark *));` after the `@import "tailwindcss";` line
├── `@custom-variant dark` present AND tailwind.config.js still has `darkMode: ['class']`
│   └── Both wired; usually harmless but indicates incomplete migration
│       Fix : delete tailwind.config.js OR remove the `darkMode` key
└── `@custom-variant dark` present AND .dark token block missing in globals.css
    └── Variant exists but has nothing to switch to
        Fix : add a `.dark { --background: oklch(...); ... }` block defining every token

Companion Skills

  • shadcn-core-theming (B2) : Full theming reference covering tokens, semantic color naming, sidebar tokens, chart tokens. Use AFTER migration to understand the v4 token shape.
  • shadcn-errors-styling-conflicts (B11) : When className overrides do not visually win, especially during a half-migrated v3/v4 project where class semantics shift mid-file.
  • shadcn-impl-init : Bootstrapping a fresh project goes through shadcn init which generates v4 by default in evergreen-2026; use this skill if migrating an existing project rather than starting fresh.

Reference Links

See references/methods.md for the v3-vs-v4 syntax reference table per concept. See references/examples.md for full before/after files (globals.css, tailwind.config.js, postcss.config.js, package.json). See references/anti-patterns.md for the six most common half-migration bugs and their root causes.