Home JavaScript Tailwind v4 Migration: 5 Critical Steps to Upgrade Safely
Intermediate 3 min · September 07, 2026
Tailwind CSS v4 Migration Guide

Tailwind v4 Migration: 5 Critical Steps to Upgrade Safely

Tailwind v4 migration breaking pages silently? Master the CSS-first @theme shift in 5 steps and upgrade safely.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
September 22, 2026
last updated
1,799
articles · all by Naren
Before you start⏱ 15 min
  • A Tailwind v3 project (or basic Tailwind familiarity)
  • Node.js 20+ for the upgrade tool
  • A Vite or PostCSS build you can modify
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Tailwind v4 moves configuration from tailwind.config.js into CSS via @theme — tokens become native CSS variables that generate utilities
  • Four migration pillars: @import "tailwindcss" entry, @tailwindcss/vite build plugin, @theme tokens, @utility for custom utilities
  • Build performance jumps ~10x: large projects compile in sub-100ms vs multi-second v3 builds, with automatic content detection
  • Production lesson: an upgrade-tool-only migration shipped 40 broken pages with a green build — renamed utilities fail silently, only visual regression catches them
  • Browser baseline is mandatory: Safari 16.4+, Chrome 111+, Firefox 128+ for @property and color-mix() support
  • Safety rule: keep tailwind.config.js via @config as a bridge, migrate tokens incrementally, screenshot-diff before shipping
✦ Definition~90s read
What is Tailwind CSS v4 Migration?

Tailwind CSS v4 is a ground-up rewrite of the utility-first framework: configuration moves from tailwind.config.js into CSS via the @theme directive, the build integration moves from PostCSS to a Vite plugin (or dedicated CLI package), and the engine compiles most projects in sub-100ms. Design tokens become native CSS custom properties — --color-brand-500 in @theme simultaneously generates utilities like bg-brand-500 and exposes var(--color-brand-500) for hand-written CSS — making themes inspectable in DevTools, composable with color-mix(), and shareable as plain CSS files.

Imagine you used to order custom paint by phoning a warehouse (tailwind.config.js) that mixed colorsjs behind closed doors.

Custom utilities are defined with @utility, variants with @custom-variant, and the legacy theme() function is replaced by native var().

Migration follows five steps: update dependencies to v4 plus @tailwindcss/vite (Node 20+ required), run the official upgrade tool, replace @tailwind directives with @import "tailwindcss", migrate theme values to @theme and utilities to @utility, and finish with visual regression across breakpoints. The hard requirements are a modern browser baseline (Safari 16.4+, Chrome 111+, Firefox 128+) and respect for silent failure modes — renamed utilities generate no CSS and no errors, so only screenshot diffing proves a migration clean.

Plain-English First

Imagine you used to order custom paint by phoning a warehouse (tailwind.config.js) that mixed colorsjs behind closed doors. Tailwind v4 moves the mixing station into your own studio (@theme in CSS): you mix the colors yourself, you can see every jar on the shelf in DevTools, and any other painter can use your jars just by borrowing the shelf. Same paint, but now the recipe is visible, inspectable, and shareable instead of locked in someone else's warehouse.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Tailwind v4 isn't a patch — it's a rewrite. Configuration moves from JavaScript into CSS, the build moves from PostCSS to Vite, and the engine gets roughly 10x faster.

That much change breaks things. Renamed utilities, dead directives, vanished configs — each one fails quietly, showing up as unstyled elements rather than build errors.

This guide gives you the five-step migration in order, the @theme mental model that replaces tailwind.config.js, and the visual-regression safety net that catches what automation misses.

The Big Shift: CSS-First Configuration With @theme

The core shift is simple: your design tokens move from JavaScript into CSS. Instead of theme.extend.colors in tailwind.config.js, you write @theme { --color-brand-500: oklch(...); } in your stylesheet. Tailwind then generates bg-brand-500, text-brand-500, and every other color utility from that one variable.

The same variable is simultaneously available as var(--color-brand-500) in your custom CSS. One definition feeds both systems — utilities and hand-written styles can never disagree about what 'brand' means.

Because tokens are plain CSS, they're inspectable in DevTools, composable with color-mix(), and shareable across projects as importable CSS files. Config values stopped being build-time secrets and became runtime citizens.

📊 Production Insight
The broken-pages incident happened because custom spacing lived only in the deleted JS config while @theme didn't yet define it — the whole site snapped to defaults. The team that migrated tokens section-by-section with @config as a bridge never lost a style. Rule: every token exists in exactly one live system during migration, never zero.
🎯 Key Takeaway
@theme turns tokens into CSS variables that generate utilities AND feed custom CSS from one definition.

The 5-Step Migration Path in Order

Do it in this order and each step is verifiable. First, update dependencies: remove tailwindcss v3/postcss/autoprefixer where replaced, add tailwindcss v4 plus @tailwindcss/vite (or the CLI package). Node 20+ is required for the tooling.

Second, run the official upgrade tool — it rewrites dependencies, converts basic config to CSS, and fixes common template patterns. Review its diff like a human PR; it covers ~80%, not 100%.

Third, replace the entry CSS: @tailwind base/components/utilities becomes @import "tailwindcss" (first import in the file). Fourth, migrate remaining theme values to @theme and rewrite custom utilities with @utility. Fifth, screenshot-diff everything before shipping.

entry.cssCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* BEFORE (v3): entry.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* AFTER (v4): entry.css — @import must come first */
@import "tailwindcss";

@theme {
  --font-display: "Inter", system-ui, sans-serif;
  --color-brand-500: oklch(0.65 0.15 250);
  --color-brand-700: oklch(0.45 0.14 250);
  --breakpoint-3xl: 120rem;
}

/* usage: bg-brand-500, text-brand-700, font-display */
/* custom CSS: color: var(--color-brand-500); */
Try it live
⚠ Silent Classes Are the Entire Danger
Unknown classes in v4 generate no CSS and no error. Your build stays green while pages render unstyled. This single fact is responsible for nearly every 'successful' v4 migration that later explodes in support tickets. Screenshot-diff every important page — the compiler will not save you.
📊 Production Insight
The incident team ran steps one through four in an afternoon and shipped. The team that succeeded added step five — 40 pages screenshotted at three breakpoints — and caught 60+ stale classes pre-deploy. Same tool, different outcome; the regression pass was the whole difference.
🎯 Key Takeaway
Deps → upgrade tool → entry CSS → @theme tokens → visual regression. Skipping step five is how 40 tickets happen.

Build Pipeline Changes: Vite Plugin and Lightning CSS

The build plumbing changes alongside the config. In v4, Tailwind runs as a Vite plugin (@tailwindcss/vite) instead of a PostCSS plugin — your dependency bump is also a pipeline change. Update vite.config.ts to register the plugin and drop postcss.config.js if Tailwind was its only client.

Autoprefixer and postcss-import are generally unnecessary now: v4 handles vendor prefixing internally via Lightning CSS and bundles imports itself. Fewer config files, fewer version conflicts.

The payoff is speed. The rewritten engine parses sources and generates CSS on demand so efficiently that most projects compile in sub-10ms and even huge ones stay under 100ms. CI style steps effectively disappear from timing charts.

📊 Production Insight
One team's CI style step went from 14 seconds to under a second after the Vite-plugin switch — a bigger wall-clock win than the token migration itself. They kept the old PostCSS config around 'just in case' for a month; it was never needed. Rule: delete the retired config files instead of commenting them out.
🎯 Key Takeaway
Tailwind becomes a Vite plugin, PostCSS middlemen retire, and style builds drop to milliseconds.

API Swaps: @utility, var(), and Dead Configs

Three API swaps bite almost everyone. @apply with !important patterns should move to @utility, which participates in variants and responsive prefixes properly. The theme() function in CSS is gone — use native var(--token) since every @theme value is a real CSS variable. And safelist config is largely unnecessary because v4's content detection is far more thorough.

Container queries deserve a look while you're here: v4 supports @container natively, so responsive-to-parent patterns that needed plugins in v3 now work out of the box.

Handle each swap with a project-wide search (theme(, @apply, safelist) and fix every hit before the regression pass. Stragglers hide in rarely-visited stylesheets.

utilities.cssCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* v3 patterns */
/* .card { @apply shadow-sm bg-white text-black; } */
/* .btn { background: theme('colors.brand.500'); } */

/* v4 equivalents */
@utility btn-brand {
  background: var(--color-brand-500);
  color: white;
  padding: 0.5rem 1rem;
  border-radius: 0.375rem;
}
/* usage: <button class="btn-brand hover:btn-brand"> */

@custom-variant hocus (&:hover, &:focus);
/* usage: <a class="hocus:underline"> */
Try it live
📊 Production Insight
In the incident, a single leftover theme() call in an admin stylesheet silently produced invalid CSS that blanked the entire admin sidebar. It lived in a file nobody screenshotted. Project-wide search would have found it in seconds. Rule: grep the three patterns before every v4 deploy, no exceptions.
🎯 Key Takeaway
Search for theme(, @apply, and safelist project-wide — every hit is a v4 breakage waiting to render.

Browser Baseline and the OKLCH Palette Shift

This step is non-negotiable: v4 requires Safari 16.4+, Chrome 111+, and Firefox 128+. The framework depends on @property, color-mix(), and cascade layers at its core — older browsers don't just degrade, they break.

Check your analytics and support matrix before migrating. Enterprise and emerging-market audiences often still include older browsers; for them, staying on v3.4 is the correct professional call, not a failure.

Also note the default palette now uses OKLCH for perceptually uniform color. Slight shade shifts versus v3 are expected — that's another reason the visual regression pass matters. Some 'breakage' is just better color science.

📊 Production Insight
A B2B team discovered 12% of their users on old embedded browsers only after migrating — the app didn't degrade, it shattered. They rolled back to v3.4 and scheduled v4 with the next contract renewal cycle. Rule: pull the browser analytics before the migration branch, not after the rollback.
🎯 Key Takeaway
Modern browsers or stay on v3.4 — and expect slight color shifts from the OKLCH palette even in perfect migrations.

Finishing Clean: Proof, Regression, and Handoff

Close it out cleanly. Keep @config pointing at tailwind.config.js until a build without it produces byte-identical CSS — that's your proof the token migration is complete. Then delete the JS file and celebrate with a smaller repo.

Run the full build with a cold cache, screenshot-diff all key pages at mobile, tablet, and desktop, and exercise interactive states (hover, focus, dark mode) that static screenshots miss.

Document the new token home for the team: @theme blocks in entry CSS, shared brand tokens as importable CSS files. Future theming work now happens in stylesheets, not JavaScript.

📊 Production Insight
The successful re-migration ended with a one-line proof: diff of compiled CSS with and without tailwind.config.js showed zero differences. That artifact went into the PR, reviewers approved in minutes, and the deploy held. Proof beats promises — attach the diff.
🎯 Key Takeaway
Byte-identical CSS without the JS config is your done-criteria; screenshot diffs plus interactive checks are your proof.
● Production incidentPOST-MORTEMseverity: high

The Green Build That Shipped 40 Broken Pages

Symptom
Cards lost shadows, buttons lost backgrounds, and three landing layouts collapsed at mobile breakpoints. No console errors, no build failures — elements simply rendered unstyled. Support tickets hit 40 within 18 hours while every dashboard stayed green.
Assumption
The team assumed the upgrade tool's clean exit meant the migration was complete, and assumed visual QA on two pages covered the site. Nobody knew renamed utilities fail silently — no build error, just missing CSS — so the 90% of pages nobody screenshotted carried invisible damage.
Root cause
The v3-to-v4 upgrade renamed or removed dozens of utilities (opacity modifiers, shadow scales, shrink/grow syntax) that the codemod didn't cover. In v4, unknown classes generate no CSS and no error — so 60+ stale classes silently produced unstyled elements across pages nobody had visually checked. The old tailwind.config.js had also been deleted upfront, dropping custom spacing the new @theme block didn't yet define.
Fix
The deploy was rolled back in 25 minutes. The team then added screenshot diffing across 40 key pages at three breakpoints, re-ran the migration, fixed 60+ renamed utilities the tool had skipped, kept tailwind.config.js via @config as a bridge while tokens moved to @theme one section at a time, and re-shipped a week later with zero visual diffs.
Key lesson
  • A green build means nothing in CSS migrations — only visual regression across real pages proves the styles survived.
  • Automated codemods handle ~80% of renames; budget explicit human time for the long tail of skipped utilities.
  • Keep the old config as a bridge (@config) and migrate incrementally — deleting it first turns one migration into an all-at-once restyle.
Production debug guideFour v4 migration failures and the exact checks that fix each one.4 entries
Symptom · 01
No Tailwind styles generate at all after upgrading
Fix
Check the entry CSS: v4 needs @import "tailwindcss" as the first import. Verify the Vite plugin (@tailwindcss/vite) or CLI package replaced the old PostCSS setup, and confirm Node 20+ for the tooling.
Symptom · 02
Random elements lose styling with no build errors
Fix
Diff generated CSS before/after for the affected pages. Search the v4 upgrade notes for each missing class — shadow, rounded, and opacity utilities were the most renamed. Codemod the old names, then screenshot-diff every breakpoint.
Symptom · 03
Custom colors, fonts, or spacing vanish after deleting tailwind.config.js
Fix
Keep @config "./tailwind.config.js" in your entry CSS while migrating tokens one section at a time to @theme. Only delete the JS file when a full build with it removed produces byte-identical CSS.
Symptom · 04
Build errors about @import order or fonts stop loading
Fix
Move font/library @imports below the Tailwind import or attach explicit layers. Then check DevTools computed styles to confirm which layer wins and adjust @layer order.
Tailwind v3 vs v4 — What Changed and What to Do
AreaTailwind v3Tailwind v4Migration action
Configurationtailwind.config.jsCSS-first @themeMove tokens into CSS
Build integrationPostCSS pluginVite plugin / CLISwap to @tailwindcss/vite
Import syntax@tailwind base/components/utilities@import "tailwindcss"Replace entry CSS
Custom utilities@layer + @variants@utility directiveRewrite with @utility
Theme() in CSStheme() functionNative var(--token)Replace with CSS vars
Build speedSeconds on large appsSub-100ms typicalFree win, verify in CI
Browser baselineIE11-era fallbacksSafari 16.4+, Chrome 111+Confirm support matrix
⚙ Quick Reference
2 commands from this guide
FileCommand / CodePurpose
entry.css/* BEFORE (v3): entry.css */The 5-Step Migration Path in Order
utilities.css/* v3 patterns */API Swaps

Key takeaways

1
v4 is CSS-first
tokens live in @theme blocks, not tailwind.config.js — utilities and CSS vars generate from one source.
2
Swap the build
@tailwindcss/vite replaces the PostCSS plugin; @import "tailwindcss" replaces @tailwind directives.
3
Run the official upgrade tool, but verify with visual regression
renamed utilities fail silently, not loudly.
4
Modern browser baseline is mandatory (Safari 16.4+, Chrome 111+)
v4 relies on @property and color-mix().
5
Keep tailwind.config.js via @config as a bridge; migrate tokens incrementally, delete the JS file last.

Common mistakes to avoid

4 patterns
×

Leaving custom @imports above the Tailwind import

Symptom
The build fails with a cryptic PostCSS import-order error, or fonts silently stop loading because their @import was pushed after generated rules.
Fix
Move any @import for fonts or libraries below the Tailwind import, or use @import with layer(). CSS requires @import first — Tailwind v4 just enforces what the spec always said.
×

Assuming every v3 utility class still exists

Symptom
Removed and renamed utilities (shadow-sm, bg-opacity-, flex-shrink-) silently stop generating CSS, leaving unstyled elements scattered across pages with no build error.
Fix
Map old classes to new equivalents during migration (codemod where possible), then run visual regression across breakpoints. Budget half a day for class renames on large codebases.
×

Defining @theme tokens that collide with defaults

Symptom
Custom colors override defaults you still use (or vice versa), and buttons change shade in production with no class changes anywhere in git history.
Fix
Either scope the @theme block to the layers you want, or namespace custom tokens distinctly (brand-, app-). Inspect DevTools computed variables to confirm which layer won.
×

Deleting tailwind.config.js before migrating its contents

Symptom
Custom spacing, fonts, and breakpoints vanish from generated CSS at once, and the whole site snaps back to default styling in a single deploy.
Fix
Keep tailwind.config.js loaded via @config during migration, then migrate tokens to @theme incrementally. Delete the JS config only when nothing references it.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What are the headline breaking changes in Tailwind CSS v4?
Q02SENIOR
How does the @theme directive work and why is it better than JS config?
Q03JUNIOR
How would you migrate a production site from v3 to v4 safely?
Q01 of 03SENIOR

What are the headline breaking changes in Tailwind CSS v4?

ANSWER
v4 moves configuration from tailwind.config.js into CSS via @theme, replaces @tailwind directives with @import "tailwindcss", runs as a Vite plugin instead of PostCSS, defines custom utilities with @utility, and replaces the theme() function with native CSS variables. It's also dramatically faster (sub-100ms builds) and requires modern browsers (Safari 16.4+, Chrome 111+).
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Is there an automated upgrade tool for v3 to v4?
02
Does Tailwind v4 support older browsers?
03
Where do my tailwind.config.js theme values go?
04
Can I keep tailwind.config.js temporarily?
05
What are the migration steps in order?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
September 22, 2026
last updated
1,799
articles · all by Naren
🔥

That's CSS. Mark it forged?

3 min read · try the examples if you haven't

Previous
Bun JavaScript Runtime Guide
1 / 1 · CSS