Tailwind CSS v4 Migration: What Actually Changed, and How to Update
Tailwind v4 moved configuration into CSS and dropped tailwind.config.js. Here is what breaks, what the @theme block replaces, and the order to migrate in.
Tailwind CSS v4 replaces tailwind.config.js with a @theme block in your stylesheet, swaps the PostCSS plugin for @tailwindcss/postcss, and renames a handful of utilities. Most projects migrate in an afternoon. The parts that take longer are custom plugins, arbitrary-value edge cases, and any tooling that read your config file directly.
Here is what actually changes, in the order we would fix it.
The short version
| Area | v3 | v4 |
|---|---|---|
| Config | tailwind.config.js | @theme block in CSS |
| Import | @tailwind base/components/utilities | @import "tailwindcss" |
| PostCSS plugin | tailwindcss | @tailwindcss/postcss |
| Dark mode | darkMode: "class" | @custom-variant dark (...) |
| Content paths | content: [...] | Automatic detection |
| Browser floor | IE-era fallbacks available | Safari 16.4+, Chrome 111+, Firefox 128+ |
That last row is the one to check before anything else. Tailwind v4 is built on native cascade layers, @property, and color-mix(). If you must support browsers older than roughly early 2023, stay on v3 — no amount of migration work changes that.
Step 1: Update the toolchain
npm install tailwindcss@latest @tailwindcss/postcss
npm uninstall autoprefixer postcss-import
Both autoprefixer and postcss-import are now built in. Leaving them installed usually still works but adds redundant passes.
// postcss.config.mjs
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
Tailwind ships an automated codemod that handles a good share of the mechanical work:
npx @tailwindcss/upgrade
Run it on a clean branch. It rewrites your CSS imports, converts most of the config, and flags what it could not translate. Treat its output as a first draft, not a finished migration.
Step 2: Replace the directives
/* v3 */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* v4 */
@import "tailwindcss";
One line. If you were importing partials with @import before the Tailwind directives, they now need @reference or plain @import handled by the bundler — v4's import handling is stricter about ordering.
Step 3: Move your theme into CSS
This is the substantive change. Everything that lived under theme.extend becomes a CSS custom property in a @theme block, and the naming is mechanical:
// v3 — tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: {
DEFAULT: "#465fff",
500: "#465fff",
600: "#3641f5",
},
},
fontFamily: {
sans: ["Outfit", "sans-serif"],
},
spacing: {
18: "4.5rem",
},
},
},
};
/* v4 — globals.css */
@import "tailwindcss";
@theme {
--color-primary: #465fff;
--color-primary-500: #465fff;
--color-primary-600: #3641f5;
--font-sans: "Outfit", sans-serif;
--spacing-18: 4.5rem;
}
The prefix determines the utility family: --color-* generates color utilities, --font-* font families, --spacing-* spacing, --breakpoint-* responsive variants, --radius-* border radii. A token named --color-brand-muted gives you bg-brand-muted, text-brand-muted and the rest, immediately.
Two consequences that surprise people:
There is no extend any more. A @theme block adds to the defaults. To replace a scale rather than extend it, clear it first with --color-*: initial; and then declare your own.
Your tokens are real CSS variables at runtime. var(--color-primary) works anywhere in your stylesheet, in inline styles, and from JavaScript via getComputedStyle. In v3 those values only existed at build time.
Step 4: Fix dark mode
darkMode: "class" has no direct equivalent. Declare the variant explicitly:
@custom-variant dark (&:where(.dark, .dark *));
Without this line, dark: falls back to prefers-color-scheme and your theme toggle stops working — with no error, which makes it a frustrating one to track down.
Step 5: Rename the changed utilities
The codemod catches most of these, but check them by hand:
| v3 | v4 |
|---|---|
shadow-sm | shadow-xs |
shadow | shadow-sm |
rounded-sm | rounded-xs |
rounded | rounded-sm |
blur-sm | blur-xs |
outline-none | outline-hidden |
ring (3px default) | ring is now 1px |
The shadow and radius shifts are the dangerous ones because the old names still exist — they just mean something smaller now. Nothing errors; your UI just gets subtly flatter. Grep for shadow-sm and rounded-sm specifically and decide each case.
The ring change is the most visible: v3's bare ring was 3px, v4's is 1px. If your focus states suddenly look thin, that is why. ring-3 restores the old width.
Step 6: Delete the content array
v4 detects template files automatically, respecting .gitignore. Delete content: [...].
If your classes live somewhere unusual — a CMS, a database, a package outside the project — point at it explicitly:
@source "../node_modules/@acme/ui/dist";
Step 7: Audit your plugins
First-party plugins ship as part of v4 or as updated packages. Third-party plugins written against v3's JavaScript plugin API need their v4 releases; some never got one.
Before migrating, list what you depend on:
grep -A20 '"plugins"' tailwind.config.js
Many v3 plugins existed to do things v4 does natively — container queries (@container, built in), text wrapping, and 3D transforms among them. Check whether you still need the dependency at all before hunting for an upgrade.
What the codemod will not do for you
- Arbitrary values referencing config, like
w-[theme(spacing.18)]. These need rewriting against CSS variables:w-[var(--spacing-18)]. - Runtime config reads. Anything importing
tailwind.config.jsinto application code — a Storybook theme, a design-token export, an email builder — has no config to import any more. Read the CSS variables instead. @applyin component files. Still supported, but@applyin a CSS module or a Vuestyleblock now needs@reference "../globals.css";at the top so the compiler can see your theme.
That last one produces a confusing error, because the @apply looks correct and simply cannot resolve your custom utilities.
A migration order that works
- Branch, then run
npx @tailwindcss/upgrade. - Fix the build: PostCSS plugin, imports, dark-mode variant.
- Diff the rendered pages, not the source. Shadows and radii shift silently.
- Grep for renamed utilities the codemod missed.
- Handle plugins last — by then you know which ones you still need.
Budget an afternoon for a mid-sized app, longer if you have custom plugins or generate classes at runtime.
Frequently asked questions
Can I keep using tailwind.config.js?
Yes, with @config "./tailwind.config.js" in your CSS. It is a compatibility bridge for incremental migration, not a long-term path — new features assume @theme.
Is v4 faster? Substantially, yes. Full builds are several times quicker and incremental rebuilds are typically in the low milliseconds. The new engine does less work per change.
Do I need to migrate at all? Not urgently. v3 still works. Migrate when you want the performance, container queries, or CSS-variable theming — or when a dependency forces it.
Why is my new color utility not generating?
If you are on Turbopack, restart the dev server. @theme changes are not hot-reloaded, so a newly added token produces no CSS until the next cold start.
Starting fresh instead
If you are weighing a migration against a rebuild, our Tailwind admin templates and landing page templates are already built on v4's CSS-first theme, so the token system above is the one you inherit on day one.
