Tailwind CSS v4 changed how shadcn/ui themes are written. If your project still has bare HSL channels like --primary: 222.2 47.4% 11.2% and a tailwind.config.js full of hsl(var(--primary)), here's how to bring the theme across.
What changed
| Tailwind v3 | Tailwind v4 |
|---|---|
Colors stored as bare HSL channels: --primary: 222.2 47.4% 11.2% | Complete color values, usually OKLCH: --primary: oklch(0.21 0.034 264.7) |
Colors mapped in tailwind.config.js with hsl(var(--primary)) | Colors mapped in CSS with @theme inline { --color-primary: var(--primary); } |
darkMode: "class" in the config | @custom-variant dark (&:is(.dark *)); in CSS |
| Radius mapped in the config | --radius-lg: var(--radius) and friends in @theme inline |
| No chart or sidebar tokens in older projects | chart-1…chart-5 and eight sidebar-* tokens |
Step 1: Convert the color values
Every --token: H S% L% needs to become a full color. You can wrap the channels as hsl(H S% L%), which works, but converting to OKLCH matches what new shadcn projects use and makes later tweaks easier.
The fastest route is the Tailwind v3 → v4 theme converter: paste your entire v3 :root and .dark blocks. It understands bare HSL channels, converts everything to OKLCH, fills in missing chart and sidebar tokens from your primary color, and gives you Tailwind v4 CSS to copy. You can also paste the same CSS into the editor's Import dialog to keep editing.
Step 2: Replace the config mapping with @theme inline
Delete the colors and borderRadius sections from tailwind.config.js; in v4 that config file is usually gone entirely. Add the mapping to your CSS instead:
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
Add the chart-* and sidebar-* mappings too if you use charts or the sidebar component.
Step 3: Move the dark-mode variant into CSS
@custom-variant dark (&:is(.dark *));
next-themes and the dark class on <html> keep working exactly as before.
Step 4: Update the base layer
The v3 base layer used @apply border-border and bg-background text-foreground. Those still work in v4:
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
Common gotchas
- Arbitrary values that wrap variables: classes like
bg-[hsl(var(--primary))]break once--primaryholds a full color. Replace them withbg-primaryorbg-(--primary). - Opacity modifiers:
bg-primary/50keeps working in v4 because Tailwind usescolor-mix(). No morehsl(var(--primary) / 0.5)gymnastics. - Third-party CSS that reads
hsl(var(--background))needs the same update.
Prefer to start over? Pick any theme from the gallery. They're all native v4 and install with one CLI command.