Upgrading to Tailwind CSS v4: What Changed and How to Migrate
Tailwind CSS v4 moved configuration into CSS, replaced the @tailwind directives with one import, detects your files automatically and renamed several utilities. How to run the official upgrade tool, the breaking changes that actually bite (shadows, rings, borders, opacity utilities), browser requirements, and what to check after.
Tailwind CSS v4 (released January 2025, with 4.3 in May 2026) is a rewrite: a faster engine, configuration in CSS instead of tailwind.config.js, and automatic file detection. Most projects upgrade in an afternoon, but a handful of renamed utilities change how things look if you don't catch them. (What Is Tailwind CSS?)
Step 1: Run the upgrade tool
On a clean git branch:
npx @tailwindcss/upgrade
It needs Node.js 20 or later. The tool updates dependencies, migrates your config into CSS, and rewrites renamed utility classes in your templates. Review the diff carefully — it's good, not perfect. (How to Read a Diff)
Step 2: Understand the new setup
One import replaces the three directives:
/* v3 */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* v4 */
@import "tailwindcss";
Theme config lives in CSS with @theme:
@import "tailwindcss";
@theme {
--color-brand-500: oklch(0.65 0.17 160);
--font-display: "Inter", sans-serif;
--breakpoint-3xl: 1920px;
}
That creates bg-brand-500, font-display and a 3xl: breakpoint — and exposes them as CSS variables you can use anywhere.
Build integration changed:
- Vite: use the
@tailwindcss/viteplugin. - PostCSS: use
@tailwindcss/postcss(thetailwindcsspackage is no longer the PostCSS plugin, and you no longer needautoprefixer).
No content array. v4 finds your template files automatically (skipping .gitignored paths). Use @source "../other-package/src"; to add paths it misses, such as a shared UI package in a monorepo.
Need your old JS config for a while? @config "./tailwind.config.js"; still loads it.
Step 3: The breaking changes that change how things look
The upgrade tool handles most renames, but know them so you can spot misses:
| v3 | v4 |
|---|---|
shadow-sm |
shadow-xs |
shadow |
shadow-sm |
rounded-sm |
rounded-xs |
rounded |
rounded-sm |
blur-sm / blur |
blur-xs / blur-sm |
outline-none |
outline-hidden |
ring (3px) |
ring-3 — plain ring is now 1px |
bg-opacity-50 |
bg-black/50 (opacity modifiers) |
flex-shrink-0 |
shrink-0 |
Two silent defaults also changed:
- Borders and rings default to
currentColor, not gray. A barebordermay suddenly look dark. Add an explicit colour (border-gray-200) where you relied on the old default. - Buttons use
cursor: default, following the browser default. Addcursor-pointerif you want the hand.
Step 4: Check browser support
v4 relies on modern CSS (cascade layers, @property, color-mix()), so it targets Safari 16.4+, Chrome 111+ and Firefox 128+. If you must support older browsers, stay on v3.4.
Step 5: Test visually
- Click through every page at mobile and desktop widths. (Responsive Design Basics)
- Look specifically at cards (shadows, radius), inputs (borders, focus rings) and buttons.
- If you use shadcn/ui, update its components and CSS variables to the v4-compatible versions. (What Is shadcn/ui?)
- Run your Playwright or screenshot tests if you have them. (Playwright Tutorial)
What you get for the trouble
Much faster builds (especially incremental rebuilds), modern colours (OKLCH), container queries built in, CSS variables for every theme value, and new utilities — v4.3 added scrollbar-* utilities and height-based container queries.
The summary
- Run
npx @tailwindcss/upgradeon a branch and review the diff. @import "tailwindcss"+@themein CSS replaces the JS config and directives.- Watch the shadow/rounded/ring renames and the
currentColorborder default.
EasySpawn gives Claude Code a persistent workspace with your app running, so it can do the Tailwind upgrade on a branch and check every page in a real browser before you merge. See how it works or join the waitlist.
Related: Tailwind vs Bootstrap · Update Dependencies Safely · Dark Mode for Your Website · CSS Units: rem, em, px
Keep reading
TypeScript satisfies Explained: Check a Type Without Losing Precision
The satisfies operator checks that a value matches a type while keeping the value's own, more specific type. How it differs from a type annotation and from as, the patterns where it shines (config objects, route maps, as const), and the mistakes to avoid.
TypeScript Enums vs Union Types: Which Should You Use?
TypeScript enums generate runtime code and have surprising behaviour; string literal unions and as const objects give the same safety with plain JavaScript. How each works, why erasableSyntaxOnly and Node's built-in type stripping reject enums, and a practical rule for choosing.