Web / CSS
Tailwind CSS v4: What's New and How to Migrate
Tailwind CSS v4 is a ground-up rewrite built on a Rust-based Oxide engine, with configuration moved out of JavaScript and into CSS via the new @theme directive. Migration is mostly mechanical — install @tailwindcss/postcss, replace @tailwind directives with @import "tailwindcss", and port your config into CSS — and the official @tailwindcss/upgrade tool automates most of it. The real work is reviewing the opinionated default changes it surfaces, not fighting the new syntax.
Why v4 Is a Different Kind of Release
Tailwind v3.4 was an incremental release: same compiler, same config file, more utilities. v4 is a rewrite. The code generator was reimplemented in Rust — the Oxide engine— and the framework's configuration model was inverted. In v3, CSS was a function of a JavaScript config; in v4, the CSS file is the configuration, and the compiler derives everything from it.
Three things follow from that inversion:
- No tailwind.config.js by default — most projects do not need one at all. Theme values, custom colors, fonts, and spacing become CSS custom properties inside
@theme, which means they are available as runtime variables in your application, not just as generated classes. - Content detection is automatic — the
contentarray is gone. v4 scans your source files with heuristics, honors.gitignore, and lets you add explicit sources only when it misses something generated at runtime. - Layered imports are explicit —
@import "tailwindcss/theme" layer(theme)and its siblings let you pull in only the layers you want, and framework preflights for React, Vue, Svelte, Angular, and Solid ship as packages rather than as config plugins.
The Install Process
Installation is two packages and one PostCSS entry. The dedicated PostCSS plugin replaces the old tailwindcss PostCSS entry point, and autoprefixer is no longer required — v4 targets modern evergreen browsers and prefixes the features it emits itself.
# One-shot automated migration
npx @tailwindcss/upgrade@latest
# What it does for you:
# 1. Installs tailwindcss + @tailwindcss/postcss
# 2. Rewrites postcss.config to the new plugin
# 3. Converts tailwind.config.js theme values into @theme in CSS
# 4. Replaces @tailwind base/components/utilities with @import "tailwindcss"
# 5. Renames deprecated utilities and classes in your source files
# Then verify - the diff is where the real review happens
git diffIf you are installing fresh rather than upgrading, the shape is: install tailwindcss and @tailwindcss/postcss, point your PostCSS config at the new plugin, and put @import "tailwindcss" at the top of your main stylesheet. Vite, Next.js, Astro, and Remix users can alternatively use the @tailwindcss/vite plugin, which skips PostCSS entirely and is the faster path on those bundlers.
/* app/globals.css - v4 style: configuration lives in CSS */
@import "tailwindcss";
@plugin "@tailwindcss/forms";
@plugin "@tailwindcss/typography";
@theme {
--color-brand-50: #f9fafb;
--color-brand-500: #6b7280;
--color-brand-900: #111827;
--font-display: var(--font-display), sans-serif;
--spacing-18: 4.5rem;
}
// postcss.config.mjs - the only build file you still need
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};Before and After: The Config Move
The clearest way to understand the migration is to look at the same design system expressed both ways. Here is a typical v3 JavaScript config:
// tailwind.config.js - v3 style: configuration lives in JavaScript
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
brand: {
50: "#f9fafb",
500: "#6b7280",
900: "#111827",
},
},
fontFamily: {
display: ["var(--font-display)", "sans-serif"],
},
spacing: {
18: "4.5rem",
},
},
},
plugins: [require("@tailwindcss/forms")],
};
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};The same system in v4 lives in the stylesheet itself. Note what is not there anymore: no content array, no module.exports, no separate plugin registration file. Theme tokens are declared as custom properties with a naming convention that maps directly to utilities — --color-brand-500 generates bg-brand-500, text-brand-500, border-brand-500, and fill-brand-500.
Because tokens are real CSS variables, you can use them directly in arbitrary values and in plain CSS — box-shadow: 0 0 0 4px var(--color-brand-500) works without any additional setup. That also makes theming, including dark mode, a variable swap rather than a class rebuild.
@theme is not the only new directive. @theme inline inlines values instead of referencing variables — useful when a token must resolve at build time; @theme static emits every variable even if unused; and @custom-variant and @utility replace the old plugin APIs for custom variants and custom utilities.
Removed, Renamed, and Changed Behavior
Utility names are largely stable — the vast majority of v3 class names work unchanged in v4. The changes that bite are a small set of removals plus a few default-behavior shifts:
| v3 | v4 | Notes |
|---|---|---|
| @tailwind base/components/utilities | @import "tailwindcss" | Layered import; sub-imports available |
| tailwind.config.js content[] | Automatic detection | Override with @source |
| theme.extend in JS | @theme in CSS | Tokens become CSS variables |
| plugins: [] in JS | @plugin "..." | CSS plugin imports |
| addUtilities / addComponents | @utility / plain CSS | No JS plugin API needed |
| bg-opacity-50 and friends | bg-black/50 syntax only | Standalone opacity utilities removed |
| flex-grow / flex-shrink | grow / shrink | Old names dropped |
| overflow-ellipsis | text-ellipsis | Renamed to match CSS |
| decoration-slice / decoration-clone | box-decoration-slice / -clone | Prefixed to match CSS |
| aria-* / data-* via plugins | Built-in variants | Including arbitrary data-[...] values |
| darkMode config key | prefers-color-scheme default | Custom strategy via @custom-variant |
Also review these default changes before shipping, because they alter rendered output even when every class name is still valid:
/* v4: new defaults you should review before shipping */
/* 1. Buttons lose their browser-default border on preflight */
button { border: 0; }
/* 2. Default border color is now currentColor, not gray-200 */
/* before */ border: 1px solid var(--color-gray-200);
/* after */ border: 1px solid currentColor;
/* 3. Opacity modifiers interpolate through oklch space,
so half-stops look perceptually even rather than muddy */
<div class="bg-gray-500/50">...</div>
/* 4. New variants are built in - no plugin required */
<div class="min-lg:flex max-md:block supports-grid:grid">...</div>
/* 5. Custom variants and utilities declared in CSS */
@custom-variant dark (&:where(.dark, .dark *));
@utility tab-4 { tab-size: 4; }The border change is the one that surprises teams most: in v3, a bare border was gray-200 by default; in v4 it inherits currentColor, matching plain CSS. On a button with colored text, that suddenly produces a colored border. The fix is explicit — add border-gray-200 wherever you relied on the old default — and it is exactly the kind of thing the upgrade tool's diff is for.
On color: v4 switched its entire built-in palette to oklch, which gives a wider gamut and more perceptually uniform mixes. Your own hex values still work; the default scale simply renders slightly brighter on wide-gamut displays. Opacity modifiers such as bg-gray-500/50 interpolate in oklch, so half-stops look cleaner than they did in sRGB. Arbitrary color syntax and @supports color(--x: red) guards remain available for anything that must stay sRGB.
Performance: What Oxide Actually Buys You
The Rust rewrite is not a benchmark-line exercise; it changes what the tool is used for. Tailwind reports roughly an order of magnitude improvement on full builds and multi-millisecond incremental builds even on very large codebases — enough that editing your theme in development feels instant rather than occasionally laggy.
The practical consequences: giant monorepos and design systems with thousands of classes no longer need a safelist workaround for slow detection; CSS generation stops competing with your dev server for CPU; and the compiler can afford to do more work per class — automatic source detection, variant expansion, oklch math — without regressing build times. If you previously rejected Tailwind because it felt slow at your project size, that reasoning is obsolete in v4.
Step-by-Step Migration Checklist
Run the upgrade tool first, then work this list. Every step is verifiable on its own, so you can migrate a branch and compare screenshots before merging.
| # | Step | Verify |
|---|---|---|
| 1 | Update Node and your framework to a v4-compatible version | Build passes on main |
| 2 | Run npx @tailwindcss/upgrade@latest on a branch | Tool exits clean; diff reviewed |
| 3 | Confirm postcss config uses @tailwindcss/postcss | CSS compiles in dev |
| 4 | Check globals.css starts with @import "tailwindcss" | Utilities present in output |
| 5 | Port theme tokens into @theme; delete tailwind.config.js | Custom colors and fonts resolve |
| 6 | Convert plugins to @plugin imports | Forms and typography styles apply |
| 7 | Add @source entries only where detection misses files | Dynamic class names render |
| 8 | Fix removed utilities (grow, text-ellipsis, *-opacity) | No missing-class regressions |
| 9 | Audit bare border utilities for the currentColor change | Visual diff of forms and buttons |
| 10 | Re-verify dark mode strategy with @custom-variant | Theme toggle still works |
| 11 | Drop autoprefixer; confirm browser targets | Output CSS diff acceptable |
| 12 | Screenshot key routes before and after, then compare | Zero unintended visual change |
When to Migrate and When to Wait
Migrate now if you are starting something new, if your Tailwind build is noticeably slow, if you want CSS-native theming and tokens you can consume outside Tailwind, or if a dependency — a component library or plugin — has already moved to v4 and is dropping v3 support.
Wait if a significant share of your styling runs through legacy v3 plugins that have not shipped v4 @plugin support; if you maintain a heavy JavaScript config that generates classes programmatically (the upgrade tool handles static theme values, not computed ones); or if you are inside a release freeze where a visual-regression pass is not affordable right now.
One structural note: a JS config can be shared across projects and generated at runtime; a CSS @theme block cannot. Teams running several apps on one design system typically keep tokens in a shared CSS file — or in a design-token pipeline that emits one — rather than in a config package. Settle that packaging question before you migrate, not after.
Conclusion
Tailwind v4 trades a JavaScript configuration file for CSS-first configuration, a JavaScript compiler for a Rust one, and a long tail of manual setup for built-in capabilities — automatic source detection, native variants, oklch color, and CSS variables as first-class theme tokens. The migration is short; the review is where you should spend your time.
Run the upgrade tool on a branch, read the diff like a code review rather than a formality, screenshot your key routes, and ship it. Projects that wait too long risk a widening gap between their plugin ecosystem and the version everything new is built against.
Key Takeaways
- v4 is a rewrite: a Rust Oxide engine for speed, CSS-first configuration via @theme, and automatic content detection with no content array.
- Install changes are small — @tailwindcss/postcss (or the Vite plugin), @import "tailwindcss" in your CSS, and autoprefixer removed.
- Most v3 class names still work; the breaking set is narrow: removed *-opacity utilities, grow/shrink, text-ellipsis, and renamed decoration utilities.
- The default to audit hardest is border color, which changed from gray-200 to currentColor — it silently restyles forms and buttons.
- Use @tailwindcss/upgrade to automate the move, but treat its diff as a review artifact and screenshot key routes before merging.
FAQ
Is Tailwind CSS v4 backward compatible with v3?
Mostly. The overwhelming majority of v3 utility class names work unchanged in v4, so most markup needs no edits. The breaking changes are concentrated in configuration (tailwind.config.js becomes CSS @theme), a handful of removed or renamed utilities such as bg-opacity-50, flex-grow, and overflow-ellipsis, and default-behavior shifts like border color changing to currentColor.
Do I still need a tailwind.config.js file in v4?
No. Configuration moves into your CSS file under the @theme directive, where custom colors, fonts, and spacing become CSS custom properties. You also no longer need a content array — Tailwind automatically detects your source files. A config file can still be used for edge cases, but most projects delete it entirely during migration.
How do I install Tailwind CSS v4 in Next.js?
Install tailwindcss and @tailwindcss/postcss, set "@tailwindcss/postcss" as the plugin in your PostCSS configuration, and start your global stylesheet with @import "tailwindcss" instead of the old @tailwind directives. On Vite-based setups you can use the @tailwindcss/vite plugin instead and skip PostCSS altogether. The @tailwindcss/upgrade command performs these steps for you.
Is Tailwind v4 faster than v3?
Yes, substantially. v4 reimplements the compiler in Rust — the Oxide engine — delivering roughly an order-of-magnitude faster full builds and multi-millisecond incremental builds even on large codebases. The change is most noticeable in monorepos, design systems with thousands of classes, and projects that previously needed safelists to work around slow content scanning.
Should I upgrade to Tailwind v4 now?
For new projects, yes. For existing ones, upgrade when your plugins support v4, your config does not generate classes programmatically, and you can afford a visual-regression pass. The official @tailwindcss/upgrade tool handles the mechanical work; budget your time for reviewing the border-color default, renamed utilities, and any custom dark mode strategy.
References
- Tailwind CSS. "Upgrade guide." tailwindcss.com. tailwindcss.com/docs/upgrade-guide
- Tailwind CSS. "Theme variables (@theme)." tailwindcss.com. tailwindcss.com/docs/theme
- Tailwind CSS. "Installing Tailwind CSS with PostCSS." tailwindcss.com. tailwindcss.com/docs/installation/using-postcss
- Tailwind CSS. "Functions and directives (@import, @plugin, @utility)." tailwindcss.com. tailwindcss.com/docs/functions-and-directives
- MDN Web Docs. "OKLCH color function." developer.mozilla.org. developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch
- web.dev. "CSS containment." Google web.dev. web.dev/articles/contains