Before you start
The migration is straightforward if you take it step by step. Your existing theme colors, spacing, and component styles all carry over. Nothing about shadcn/ui's component architecture changes. The only thing moving is where your design tokens are defined and how Tailwind reads them.
You need: Tailwind v4 installed, your existing globals.css with shadcn variables, and your tailwind.config.js (or .ts).
Step 1: Install Tailwind v4
Update Tailwind and its dependencies:
npm install tailwindcss@latest @tailwindcss/postcss@latestUpdate your PostCSS config to use the new plugin:
/* postcss.config.mjs */
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}Step 2: Replace @tailwind directives
In your globals.css, the three @tailwind directives are replaced by a single import:
/* Before (v3) */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* After (v4) */
@import "tailwindcss";Put this at the very top of your globals.css, before everything else.
Step 3: Move custom colors to @theme
This is the biggest change. In v3, custom colors lived in tailwind.config.js. In v4, they move to a @theme block in your CSS.
Your shadcn/ui CSS variables in :root and .dark stay exactly where they are. Those don't move. What moves is the Tailwind color mapping that references them.
/* v3: tailwind.config.js */
module.exports = {
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
},
},
},
}
/* v4: globals.css @theme block */
@theme {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
}The pattern: prefix each with --color- in the @theme block. This tells Tailwind to register them as color utilities. bg-primary, text-foreground, and every other utility class works exactly as before.
Step 4: Move border radius to @theme
If your Tailwind config had custom border-radius values:
/* v3: tailwind.config.js */
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
}
/* v4: globals.css @theme block */
@theme {
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
}Step 5: Handle animations
shadcn/ui defines several keyframe animations (accordion open/close, collapsible, etc.). These move from the JS config to CSS:
/* v4: in your CSS file */
@theme {
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
}
@keyframes accordion-down {
from { height: 0; }
to { height: var(--radix-accordion-content-height); }
}
@keyframes accordion-up {
from { height: var(--radix-accordion-content-height); }
to { height: 0; }
}Step 6: Delete tailwind.config.js
Once you've moved everything to your CSS file, you can delete the JavaScript config entirely. Tailwind v4 doesn't need it. If you have plugins that still require a config file, keep it for now, but the theme section should be empty.
Step 7: Update the cn() utility
If your cn() function uses tailwind-merge, make sure you're on the latest version. Older versions of tailwind-merge don't recognize v4 utility patterns.
npm install tailwind-merge@latestStep 8: Test everything
Run your dev server and check these things in order:
1. Light mode colors render correctly (background, text, buttons, cards).
2. Dark mode toggle works and colors switch properly.
3. Border radius looks right on buttons, cards, and inputs.
4. Opacity modifiers work (bg-primary/50 should be semi-transparent).
5. Animations (accordion, collapsible) still function.
6. Chart colors render if you use shadcn's chart components.
Common migration pitfalls
Border colors changed
Tailwind v4 changed the default border color from gray-200 to currentColor. If you have elements with border utility but no explicit border color, they'll suddenly inherit the text color instead of being light gray. Add border-border explicitly where needed.
Ring width default changed
In v3, ring applied a 3px ring. In v4, ring applies a 1px ring. If your focus styles look thinner, use ring-3 to match the old behavior.
Space between behavior
The space-x and space-y utilities now use gap instead of the old lobotomized owl selector (> * + *). If you have components that depend on the old behavior (like inserting dividers between children), they might look different.
The migration is mechanical, not creative. Your design stays the same. Only the plumbing changes. For a deeper look at how CSS variables interact with both v3 and v4, read CSS Variables vs Tailwind Config vs shadcn Theme. For the full picture of v4's color system changes, see Tailwind v4 Color System. Or generate a complete v4-compatible theme in seconds with SeedFlip. Every export includes the full @theme block, CSS variables, and both light and dark modes ready to paste into your globals.css.