shadcn Tailwind v4 Migration: Step by Step From v3
You have a working shadcn/ui app on Tailwind v3, and new shadcn components and most current tutorials now assume Tailwind v4. A shadcn tailwind v4 migration is mostly mechanical, but the parts that are not mechanical fail quietly: borders turn black, focus rings get thinner, accordions stop animating, and a color that came from tailwind.config.js simply does nothing. This guide is the order we would follow on a real project, with the CSS before and after each step and a list of the components that need a hand fix. We build Wingo UI, a React 19 and Tailwind v4 component library, so outside facts link to their sources, checked on November 6, 2026.
Should you migrate a shadcn project to Tailwind v4 now?
Yes, if your users are on current browsers. Tailwind v4 needs Safari 16.4, Chrome 111 or Firefox 128 and newer, according to the Tailwind upgrade guide (opens in a new tab). If you must support older browsers, stay on v3.4. The upgrade tool also needs Node.js 20 or newer.
There is no deadline. The shadcn Tailwind v4 page (opens in a new tab) says the change is non-breaking: existing v3 and React 18 apps keep working, and components you add stay on v3 until you upgrade. The cost of waiting is that every new component and example you copy is written for v4.
Do the work on its own branch, with no feature changes mixed in, so the diff only shows migration.
What are the Tailwind v4 breaking changes that hit shadcn?
The upgrade guide (opens in a new tab) lists about twenty. These are the ones a shadcn project runs into:
The hover change is a good one for mobile: a tap no longer leaves a button stuck in its hover color. The cursor change is the one people notice first.
Step 1: How do you run the upgrade codemod?
Commit everything, then run the official tool from the project root:
The tool updates package.json, replaces the three @tailwind directives with @import "tailwindcss";, moves the PostCSS setup to @tailwindcss/postcss, converts your JavaScript config into CSS, and renames utilities in your source files (shadow-sm to shadow-xs, outline-none to outline-hidden, w-[--x] to w-(--x)). The shadcn Tailwind v4 page still shows the command with @next, a leftover from the v4 beta; the current Tailwind guide drops the tag.
Two limits matter. The tool can only rewrite class names it can read as full strings, so anything built at runtime (`bg-${tone}-500`) stays as it was. And it maps your tokens with hsl(var(--x)) wrappers inside @theme, which works but is awkward. Step 2 cleans that up. If the tool stops with "Could not load the configuration file", an error reported in the shadcn upgrade discussion (opens in a new tab), check that the config loads on its own with node -e "require('./tailwind.config.js')" and fix what that prints before you run the tool again.
Then start the dev server and look at the app before you change anything else.
Step 2: How do you move tailwind.config into @theme?
This is the heart of the Tailwind v3 to v4 migration. A v3 shadcn project keeps the raw values in globals.css and the mapping to utilities in tailwind.config.js:
In v4 there is no separate Tailwind v4 config file: both halves live in CSS. The shadcn docs give four edits on top of the codemod output: move :root and .dark out of @layer base, wrap each value in hsl(), change @theme to @theme inline, and drop the hsl() wrappers inside it. The result:
Why inline matters: with plain @theme, bg-background compiles to var(--color-background), and that variable is resolved where it is defined, on :root. A .dark class below the root, or a theme scoped to one section, never changes it. With @theme inline, Tailwind writes var(--background) into the utility itself, so the value resolves on the element that uses it. A commenter in the shadcn upgrade discussion reported on December 28, 2024 that dark mode did not work for them until they switched to @theme inline. The Tailwind v4 dark mode guide covers the class variant and next-themes in detail.
darkMode: ["class"] becomes the @custom-variant line. The * { @apply border-border; } rule only compiles once --color-border exists in @theme; "Cannot apply unknown utility class: border-border" means the mapping is missing or a leftover config is still loaded.
When the page looks right, delete tailwind.config.js and the @config line the codemod added, then set the config path empty in components.json, as the components.json docs (opens in a new tab) say for v4:
Step 3: Should you convert hsl tokens to oklch?
Only if you want to. Once each variable holds a full color, hsl() works fine in v4, and the format does not change contrast. shadcn's current palette is oklch (the neutral theme uses --background: oklch(1 0 0) and --foreground: oklch(0.145 0 0) per the theming docs (opens in a new tab)), and its dark colors were revised for accessibility on March 12, 2025. If you want those values, copy them from the theming page instead of converting yours, and keep your brand color as it is.
Two things do need changing now that variables hold complete colors:
Also search your code for hsl(var(-- in inline styles, SVG fills and color-mix() calls. Each one now wraps a color in a second hsl() and renders nothing. A shadcn Tailwind v4 theme whose tokens are full colors is also easier to extend: color-mix(in oklab, var(--primary) 10%, transparent) works anywhere. Our Tailwind v4 design tokens guide goes further, with surfaces, status tones and a contrast script.
Step 4: How do you fix borders and focus rings?
Search for every bare border, border-t, divide-y and ring without a color. The base rule from step 2 fixes borders globally, which is why most shadcn components look right after the upgrade. Anything rendered outside your stylesheet's reach (a portal into a third-party root, an iframe) still draws currentColor borders.
Rings are the subtle one. Stock shadcn v3 components use ring-2, which keeps its width, but custom code with a bare ring drops from 3px to 1px, and a 1px ring often fails as a focus indicator. Write the width and color explicitly:
For the cursor, add the snippet from the upgrade guide if you want pointers back on every button:
Then tab through every page with the keyboard, in both themes. That one pass catches most ring and outline regressions.
Step 5: What replaces tailwindcss-animate?
tw-animate-css. shadcn deprecated tailwindcss-animate on March 19, 2025, and new projects get the replacement by default:
Remove @plugin "tailwindcss-animate"; from globals.css and add @import "tw-animate-css"; after the Tailwind import. The classes your dialogs, popovers and dropdowns use (animate-in, fade-in-0, zoom-in-95, slide-in-from-top-2) keep working, and the package ships animate-accordion-down and animate-accordion-up, so the keyframes you had in tailwind.config.js can go. Its README (opens in a new tab) warns that it is not a 100% compatible drop-in, so click through every animated component once.
Another option is to animate in JavaScript. Our components use motion springs instead of keyframes: the Accordion springs its height open and fades the answer in, and with reduced motion on it skips the height animation and only fades. There is no animation plugin to migrate at all.
$ npx wingo-ui@latest add accordionWhich shadcn components need a manual fix?
The codemod handles most class strings. These are the places it misses or where the defaults change the look:
- Sidebar: it sizes itself with classes such as
w-[--sidebar-width], which v4 writes asw-(--sidebar-width). Check that the codemod caught every one and that the sidebar still opens at its full width. A commenter whose sidebar broke after the upgrade fixed it on February 3, 2025 by takingsidebar.tsxandglobals.cssfrom shadcn's Tailwind v4 example app. - Radius classes: the codemod renames
rounded-smtorounded-xsandroundedtorounded-sm. Your theme maps--radius-smitself, so the rename can change the look:rounded-smused to mean--radiusminus 4px, androunded-xsfalls back to Tailwind's default 2px. Compare a few inputs and badges with the v3 build and keep whichever class gives the radius you had. - Accordion and Collapsible: they lose their animation if you removed the config keyframes before adding
tw-animate-css. - Chart:
hsl(var(--chart-n))inchartConfig, covered in step 3. - Toast: deprecated in favor of Sonner. Your existing toast keeps running, but shadcn now points new projects to
sonner. - Anything with
cvavariants built from template strings: the tool cannot read them. Grep for`${insideclassName. - Components you edited:
npx shadcn@latest add --all --overwriterewrites every file with the v4 version. Commit first, then reapply your changes from the diff. Our post on updating shadcn components without losing edits explains why a three-way merge saves this step.
The rest of shadcn's v4 changes are optional: size-4 instead of w-4 h-4, data-slot on every part, and plain functions instead of forwardRef (React 19 passes ref as a prop; see our React 19 forwardRef migration).
What does a component written for Tailwind v4 look like?
It paints through CSS variables, uses v4 variants instead of JavaScript for touch and focus, and needs no config file. Our Button is a fair example: variants paint through CSS variables with the new shorthand (bg-(--tone), text-(--tone-foreground)), focus uses outline-hidden plus a 2px outline-ring, and the pointer-coarse: variant grows the default size to 44px on touch screens with no JavaScript. The --tone-* variables come from Tones, so tone="success" or color="#0EA5E9" changes every variant without new classes:
$ npx wingo-ui@latest add buttonApp-wide defaults go through UIProvider, so a migrated app can set radius or size once instead of editing every file:
If you add our components to the migrated project, run npx wingo-ui@latest init after the migration, not before. It reads the stylesheet path from components.json, warns when that file is still a v3 stylesheet without @import "tailwindcss", and adds only the tokens that are missing: values your shadcn theme already defines stay as they are. Then npx wingo-ui@latest add button card drops in the Card and Button source. More guides on this stack are in Next.js and Tailwind CSS.
How do you check that the migration worked?
Run through this list before you merge:
npm run buildpasses and the dev server shows no "unknown utility" errors.- Every page in light and dark: no black borders, no missing backgrounds.
- Keyboard pass: every control shows a visible focus ring.
- Open every dialog, sheet, popover, dropdown and accordion: each one still animates.
- Charts show their colors.
- On a phone: buttons do not stay tinted after a tap, and the sidebar opens at full width.
git grep -n "hsl(var(--"andgit grep -n "tailwind.config"return nothing you still need.
What should you do next?
Run the codemod on a branch today and fix what the checklist finds; for a typical shadcn app the hand fixes are a few small commits. If you want components that were written for Tailwind v4 from the start, with tokens, focus rings and touch sizes already done, add Button, Card and Accordion with a free account (email only, no card): they are among the 75 free items in the 357-item catalog, and the pricing page shows what Pro adds.
FAQ
Does shadcn/ui support Tailwind v4?
Yes. The shadcn CLI sets up new projects on Tailwind v4 and React 19, and every component was updated with data-slot attributes and oklch colors. Existing Tailwind v3 projects keep working, and the shadcn docs call the upgrade non-breaking.
Do I still need a tailwind.config file with Tailwind v4?
No. Tailwind v4 reads its configuration from CSS: theme values go in @theme, plugins in @plugin, custom utilities in @utility. A JavaScript config still works if you load it with @config, but it is no longer detected automatically, and shadcn's components.json expects the tailwind.config field to be empty on v4.
Why are my borders black after upgrading to Tailwind v4?
Tailwind v4 changed the default border color from gray-200 to currentColor, so a bare border class takes the text color. shadcn projects fix it with a base rule, * { @apply border-border; }, which needs --color-border mapped in @theme inline to work.
What replaces tailwindcss-animate in Tailwind v4?
tw-animate-css, which shadcn adopted on March 19, 2025. Install it as a dev dependency, remove @plugin "tailwindcss-animate", and add @import "tw-animate-css" after @import "tailwindcss". It covers animate-in, fade, zoom, slide and the accordion and collapsible keyframes, though its README warns it is not a 100% drop-in.
Do I have to convert my shadcn colors from hsl to oklch?
No. hsl() values work in Tailwind v4 as long as each variable holds a complete color. Convert to oklch only if you want shadcn's current palette or easier lightness tweaks, and run a contrast check after you change any value.
- Tailwind CSS
- shadcn/ui
- Migration
- Next.js
- Design tokens

