I put off upgrading a client project to Tailwind v4 for four months. I’d read the release notes, watched Adam’s video twice, and still convinced myself the CSS-first config was going to break something obscure at the worst possible moment. Then a PR reviewer asked why I was still shipping v3.4 and I ran out of good excuses.
So I did it on a Sunday. A Next.js 15 app, roughly 40 components, custom design tokens, dark mode, and a plugin I’d stopped remembering how to configure. Here’s what actually happened, what the v4 announcement undersells, and the one snag that ate ninety minutes I wasn’t expecting to spend.
What v4 actually changed
If you’ve only skimmed the release, three things matter more than the rest.
The config file is gone. Well, not gone gone. You can still write a tailwind.config.js if you want. But the recommended path is a CSS file with a @theme block, and the framework reads that directly. Everything from colors to breakpoints to the typography scale now lives in CSS. That sounds like a small shuffle. It isn’t.
The build engine got rewritten. The new engine (they call it Oxide) is a Rust rewrite of what used to be a PostCSS plugin chain. In my project, a full production build went from 4.2s to 0.7s. Dev incrementals dropped below 100ms, which is the first time in years that Tailwind has felt genuinely instant instead of “almost instant”.
The three @tailwind directives collapsed into one line. This one is small but everyone stubs a toe on it during migration, so:
/* v3 */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* v4 */
@import "tailwindcss";
That’s the whole change to your entry CSS file. Nobody warned me it was going to feel that clean.
The @theme block that replaced tailwind.config.js
The best part of v4, for me, is that design tokens now live in the same file as the CSS that uses them. In v3, editing a color meant hopping between tailwind.config.js, the CSS entry file, and the JSX that consumes the class. Now it’s one file, and the tokens are actual CSS custom properties.
Here’s a shortened before-and-after from a real project.
v3, config-first:
// tailwind.config.js
module.exports = {
content: ["./src/**/*.{ts,tsx}"],
theme: {
extend: {
colors: {
brand: {
50: "#f5f7ff",
500: "#4f46e5",
900: "#1e1b4b",
},
},
fontFamily: {
display: ["Inter Display", "sans-serif"],
},
},
},
};
v4, CSS-first:
@import "tailwindcss";
@theme {
--color-brand-50: #f5f7ff;
--color-brand-500: #4f46e5;
--color-brand-900: #1e1b4b;
--font-display: "Inter Display", sans-serif;
}
Two things I didn’t appreciate until I lived with it for a week.
First, every one of those variables is a real CSS custom property. So I can use var(--color-brand-500) inside a @media query, or inside a calc(), or inside a component library that doesn’t know Tailwind exists. In v3 those tokens only existed at build time. Now they’re regular CSS, which means the rest of my stylesheet gets to reuse them for free.
Second, dark mode gets simpler. Instead of a JS predicate in the config, I redefine tokens under a @media block or a class selector, and the whole utility set updates. No dark: prefix required for tokens that are already themed at the variable level.
The Oxide engine and what “10x faster” actually means
Every framework release claims a speed bump nobody notices. This one I noticed. My CI pipeline shaved almost four seconds off the CSS build step alone, which added up over a hundred builds a day.
More important: the dev experience feels different. When I edit a class name in a component and save, the browser paints the new style before I can move my hand to the mouse. That was already true of Vite. It wasn’t true of Tailwind’s own rebuild step. Now it is.
If you’re on a large monorepo, the speed matters more than you’d expect. A teammate on a much bigger codebase told me their editor’s Tailwind IntelliSense stopped stalling on the third autocomplete. That was worth the upgrade all by itself.
For a deeper look at why the Rust rewrite behaves this way, the official upgrade guide has the honest performance numbers. The 10x claim is real for cold builds. For incremental dev builds, it’s closer to 5x. Both feel great in practice.
Where the migration bit me
Not everything went smoothly. Here’s the ninety-minute snag.
I had a handful of custom utilities registered via a v3 plugin. Something like a .text-balance-safe helper that fell back to text-wrap: balance with a browser check. In v4 the plugin API changed shape, and the automatic migration codemod turned my plugin into a broken CSS block that half-worked and half-didn’t.
The fix, once I stopped fighting it, was to move that utility into plain CSS using @utility. Which is the v4 way of extending Tailwind: skip the plugin entirely, write real CSS, and let the framework compose it into the utility layer.
@utility text-balance-safe {
text-wrap: balance;
@supports not (text-wrap: balance) {
text-wrap: pretty;
}
}
Cleaner than the plugin, honestly. But I wouldn’t have found @utility without an hour of digging through the migration docs and the changelog. That’s the piece the marketing didn’t sell hard enough: v4 wants you to write CSS, not JavaScript.
One other papercut worth mentioning. If you were using arbitrary breakpoints like min-[820px]:flex, those still work, but the way custom breakpoints get declared in the @theme block feels different enough that I broke a couple of them on the first pass. Twenty minutes with the console open sorted it out.
And if you rely on @apply inside component CSS, be aware that v4 tightened up when it’s willing to compose utilities. A few of my CSS Modules stopped inlining tokens because I was calling @apply from a scope Tailwind didn’t consider a utility layer. The fix was a one-line @reference declaration at the top of the file, but the error message pointing me at it wasn’t obvious.
What I’d tell you to do this week
If you’re on v3 and shipping actively, upgrade. Not next quarter, this week. The dev-loop speed alone pays for the migration inside a sprint, and the config-in-CSS story makes new components measurably faster to write. It’s the first Tailwind release where I felt like the framework got out of my way instead of adding another layer.
If you’re on v3 and coasting on a legacy project, you can wait. v3 isn’t going anywhere fast, and the migration isn’t zero-cost for large plugin footprints. But even then I’d budget a day this quarter to at least run the codemod on a branch and see how much of the diff is scary. In my case, ninety percent of it was mechanical.
If you write a lot of custom utilities or design-system code, learn the @utility and @variant at-rules before you migrate. The plugin story is deemphasized in v4 on purpose. The CSS-first way is more powerful once you accept it. I wrote about a related shift toward CSS-native tooling in my CSS Subgrid deep-dive if you want more context on where the platform is heading.
And if you want to see the end result on a real production stack, most of my client work these days ships with a Tailwind v4 base. I keep a running list of the projects I’ve shipped on my site if you want to see the config patterns in the wild.
The short version: v4 is the first Tailwind upgrade in years where the payoff is bigger than the migration cost. I put mine off for four months. Don’t repeat my mistake.