Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
  1. Home
  2. Blog
  3. Next.js and Tailwind CSS
  4. shadcn Tailwind v4 Migration: Step by Step From v3
Auf Deutsch lesen
  1. Blog
  2. Next.js and Tailwind CSS
  3. shadcn Tailwind v4 Migration: Step by Step From v3
Next.js and Tailwind CSS
Next.js and Tailwind CSS

shadcn Tailwind v4 Migration: Step by Step From v3

SR
Serban Rusu · Founder of Wingo UI
Nov 6, 2026 · 10 min read

On this page

0%
  1. Should you migrate a shadcn project to Tailwind v4 now?
  2. What are the Tailwind v4 breaking changes that hit shadcn?
  3. Step 1: How do you run the upgrade codemod?
  4. Step 2: How do you move tailwind.config into @theme?
  5. Step 3: Should you convert hsl tokens to oklch?
  6. Step 4: How do you fix borders and focus rings?
  7. Step 5: What replaces tailwindcss-animate?
  8. Which shadcn components need a manual fix?
  9. What does a component written for Tailwind v4 look like?
  10. How do you check that the migration worked?
  11. What should you do next?
  12. FAQ
    1. Does shadcn/ui support Tailwind v4?
    2. Do I still need a tailwind.config file with Tailwind v4?
    3. Why are my borders black after upgrading to Tailwind v4?
    4. What replaces tailwindcss-animate in Tailwind v4?
    5. Do I have to convert my shadcn colors from hsl to oklch?

TL;DR

Migrate a shadcn project to Tailwind v4 on a branch: run npx @tailwindcss/upgrade, move :root and .dark out of @layer base with hsl() around each value, map them in @theme inline, then delete tailwind.config.js and leave the config field empty in components.json. Swap tailwindcss-animate for tw-animate-css, and check by hand what the codemod cannot see: bare border and ring classes, class names built at runtime, chart colors and accordion keyframes.

Published Nov 6, 2026

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:

Changev3v4Where it shows up
Configurationtailwind.config.jsCSS: @theme, @plugin, @utilityevery color and radius token
Entry point@tailwind base; components; utilities@import "tailwindcss";globals.css
PostCSS plugintailwindcss + autoprefixer@tailwindcss/postcsspostcss.config.mjs
Default border colorgray-200currentColorany bare border class
Bare ring3px, blue-5001px, currentColorcustom focus styles
outline-noneinvisible outlineoutline-style: none; old behavior is outline-hiddenbuttons, inputs, triggers
Scale renamesshadow-sm, rounded-sm, blur-smshadow-xs, rounded-xs, blur-xscards, popovers, inputs
CSS variable shorthandw-[--sidebar-width]w-(--sidebar-width)sidebar, custom widths
hover:alwaysonly on devices that can hoveranything that relied on sticky hover on phones
Button cursorpointerdefaultevery <button>
Custom utilities@layer utilities@utility name { ... }your own helpers

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:

bash
git switch -c chore/tailwind-v4
npx @tailwindcss/upgrade

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:

js
// tailwind.config.js (v3)
module.exports = {
darkMode: ["class"],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [require("tailwindcss-animate")],
};

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:

css
/* app/globals.css (v4) */
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.5rem;
--background: hsl(0 0% 100%);
--foreground: hsl(222.2 84% 4.9%);
--primary: hsl(222.2 47.4% 11.2%);
--primary-foreground: hsl(210 40% 98%);
--border: hsl(214.3 31.8% 91.4%);
--ring: hsl(222.2 84% 4.9%);
}
.dark {
--background: hsl(222.2 84% 4.9%);
--foreground: hsl(210 40% 98%);
--primary: hsl(210 40% 98%);
--primary-foreground: hsl(222.2 47.4% 11.2%);
--border: hsl(217.2 32.6% 17.5%);
--ring: hsl(212.7 26.8% 83.9%);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-border: var(--border);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

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:

json
{
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
}
}

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:

ts
// chart config: the variable is already a color
const chartConfig = {
desktop: { label: "Desktop", color: "var(--chart-1)" }, // was "hsl(var(--chart-1))"
mobile: { label: "Mobile", color: "var(--chart-2)" },
};

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:

tsx
// before: 3px blue in v3, 1px currentColor in v4
<button className="focus-visible:ring">Save</button>
// after: explicit in both
<button className="outline-hidden focus-visible:ring-[3px] focus-visible:ring-ring/50">Save</button>

For the cursor, add the snippet from the upgrade guide if you want pointers back on every button:

css
@layer base {
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}

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:

bash
npm uninstall tailwindcss-animate
npm install -D tw-animate-css

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.

Open a row, then open another: the height springs and the answer fades in before the height settles. Turn on reduced motion in your system settings to see the answer fade in place instead
$ npx wingo-ui@latest add accordion
FreeAccordion docs

Which 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 as w-(--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 taking sidebar.tsx and globals.css from shadcn's Tailwind v4 example app.
  • Radius classes: the codemod renames rounded-sm to rounded-xs and rounded to rounded-sm. Your theme maps --radius-sm itself, so the rename can change the look: rounded-sm used to mean --radius minus 4px, and rounded-xs falls 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)) in chartConfig, 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 cva variants built from template strings: the tool cannot read them. Grep for `${ inside className.
  • Components you edited: npx shadcn@latest add --all --overwrite rewrites 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:

tsx
import { Button } from "@/components/ui/button";
export function SaveBar() {
return (
<div className="flex gap-2">
<Button variant="outline">Cancel</Button>
<Button tone="brand" loading loadingText="Saving">
Save changes
</Button>
</div>
);
}
Tab to a button to see the 2px focus outline, press it to feel the spring, then switch the site theme: the fills, tints and outline all come from the tokens
$ npx wingo-ui@latest add button
FreeButton docs

App-wide defaults go through UIProvider, so a migrated app can set radius or size once instead of editing every file:

tsx
"use client";
import type { ReactNode } from "react";
import { UIProvider } from "@/lib/ui-config";
export function Providers({ children }: { children: ReactNode }) {
return (
<UIProvider defaults={{ button: { radius: "full" }, card: { variant: "outline" } }}>
{children}
</UIProvider>
);
}

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:

  1. npm run build passes and the dev server shows no "unknown utility" errors.
  2. Every page in light and dark: no black borders, no missing backgrounds.
  3. Keyboard pass: every control shows a visible focus ring.
  4. Open every dialog, sheet, popover, dropdown and accordion: each one still animates.
  5. Charts show their colors.
  6. On a phone: buttons do not stay tinted after a tap, and the sidebar opens at full width.
  7. git grep -n "hsl(var(--" and git 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.

Components in this post

  • Button

    The React button component every screen starts with: five variants, six tones, all sizes, and a loading state that never jumps.

    Free
  • UIProvider

    A React config provider for Wingo UI: app-wide default props for every component, the Intl locale and a reduced-motion policy.

    Free
  • Tones

    One way to color a React component: a tone or any CSS color becomes five CSS variables every variant paints with.

    Free
  • Card

    A React card component for listings, stats, settings and plans: media, header, body and footer parts, and a lift-and-press mode.

    Free
  • Accordion

    A React accordion component for FAQs, settings sections and order details, with four variants, icons, descriptions and one or many open.

    Free

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.

Try it in your project

Button

Free

The React button component every screen starts with: five variants, six tones, all sizes, and a loading state that never jumps.

75 components free, no account needed. Pro from $8 a month.

Open Button liveSee pricing
  • Tailwind CSS
  • shadcn/ui
  • Migration
  • Next.js
  • Design tokens

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Next.js and Tailwind CSS

Tailwind v4 Design Tokens: Colors, Dark Mode and Contrast

Tailwind v4 design tokens from a production React library: @theme inline, semantic surfaces, status colors, dark mode and a contrast check you can run in CI.

SRSerban Rusu·Oct 9, 2026·20 min read
Next.js and Tailwind CSS

Tailwind v4 Nextjs 16 Setup: Config, Fonts and 5 Fixes

Set up a Tailwind v4 Nextjs 16 app: PostCSS plugin or the new Turbopack loader, @theme tokens in CSS, next/font variables and fixes for five errors.

SRSerban Rusu·Nov 6, 2026·9 min read
Next.js and Tailwind CSS

shadcn Vite Setup with Tailwind CSS v4 and React 19

shadcn Vite setup tested on Vite 8 and TypeScript 6: the Tailwind v4 plugin, the @ alias in tsconfig.app.json, dark mode without Next.js, then add components.

SRSerban Rusu·Oct 13, 2026·7 min read
Newsletter

Get new posts by email

New posts, product news and occasional offers from Wingo UI. We count clicks to keep it relevant.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • For teams
  • Theme generator

Resources

  • Docs
  • Contact
  • Programs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Compare

  • Wingo UI vs paid kits
  • Wingo UI vs shadcn/ui
  • shadcn/ui alternatives
  • MUI alternatives
  • Tailwind Plus alternatives
  • Magic UI alternatives
  • Free components

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI