Tailwind CSS v4 is a ground-up rewrite. New engine (Oxide, written in Rust), CSS-native config, a first-party Vite plugin, and PostCSS is no longer required. If you've spent the last three years tweaking tailwind.config.js and arguing about whether to extract @apply-heavy components, v4 changes the mental model enough that you need to actually read the migration guide before running the codemod.
I've upgraded two production apps and started a greenfield project on v4. Here's the honest verdict on what changed, what breaks, and whether you should upgrade today.
What actually changed in Tailwind v4
The headline isn't the speed — though the Oxide engine is roughly 5x faster on full builds and 100x faster on incremental rebuilds. The headline is that v4 is configured in CSS, not JavaScript.
Your tailwind.config.js is gone. In its place:
/* app.css */
@import "tailwindcss";
@theme {
--color-brand: oklch(0.7 0.2 250);
--font-display: "Inter", sans-serif;
--breakpoint-3xl: 120rem;
}That's the whole config. Custom colours, fonts, breakpoints — all CSS custom properties under @theme. The win: your design tokens are now consumable by any CSS, not locked inside a JS object the browser never sees. You can reference var(--color-brand) in plain CSS files, in CSS-in-JS, anywhere.
The other big shifts:
- First-party Vite plugin.
@tailwindcss/vitereplaces the PostCSS plugin for Vite projects. PostCSS still works (and is still required for Next.js), but the Vite plugin is meaningfully faster because it skips the PostCSS pipeline entirely. - Automatic content detection. No more
content: ["./src/**/*.{js,ts,jsx,tsx}"]. v4 scans your project using heuristics based on.gitignoreand common source patterns. You can override with@sourcedirectives if needed. - Container queries built in. No plugin required.
@containerand@max-md:variants ship in core. - Native CSS cascade layers. Tailwind's utilities, components, and base styles now use real
@layerrules, which means specificity is predictable and overriding utilities no longer requires!importanthacks as often. - Dynamic utility values.
grid-cols-15,mt-17,w-29— arbitrary numeric values work without[]brackets. Massive quality-of-life win.
The breaking changes that will bite you
The official upgrade tool (npx @tailwindcss/upgrade) handles the obvious renames. What it doesn't handle cleanly:
Default border colour changed. In v3, border defaulted to gray-200. In v4 it defaults to currentColor. Every <div className="border"> in your codebase will silently render with the text colour as its border. The codemod adds explicit border-gray-200 classes where it can detect the old default, but it misses dynamically composed class strings.
Default ring width changed. ring was 3px in v3, now it's 1px. If you have any focus styles relying on the default, they'll look anaemic until you switch to ring-3.
@apply in scoped styles needs @reference. If you use @apply inside a Vue SFC <style scoped> block or a CSS module, you now need to add @reference "../app.css"; at the top of that block so Tailwind knows which theme to resolve against. The docs do mention this, but only in a sub-section most people skip. I lost twenty minutes to a Vue project building with empty utility classes before finding it.
Shadow, blur, and radius scales renamed. shadow-sm is now shadow-xs, the old shadow is now shadow-sm, and a new larger shadow exists. Same shift for rounded and blur. The codemod handles class renames in JSX/HTML but won't touch them inside template literals or cn() helpers reliably.
PostCSS plugin moved. tailwindcss as a PostCSS plugin is gone. You need @tailwindcss/postcss instead. One-line fix, but if you've got CI pipelines with cached postcss.config.js, they'll fail loudly.
Migrating from v3: the actual steps
For a typical Vite + React project, here's the path that worked:
# 1. Run the upgrade codemod
npx @tailwindcss/upgrade@latest
# 2. Install the new Vite plugin
pnpm remove tailwindcss postcss autoprefixer
pnpm add -D tailwindcss@latest @tailwindcss/viteUpdate vite.config.ts:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
});Replace the contents of your main CSS file:
@import "tailwindcss";
@theme {
/* port your tailwind.config.js theme.extend values here */
--color-primary: #3b82f6;
--font-sans: "Inter", system-ui, sans-serif;
}Then: delete tailwind.config.js, delete postcss.config.js, run the dev server, and start hunting down the border-colour regressions. Visual diff your key pages — Storybook or Chromatic is your friend here. If neither is set up, Percy and Playwright snapshot tests will catch the worst of it.
For Next.js, you're still on PostCSS. Swap tailwindcss for @tailwindcss/postcss in your config and the rest of the steps apply. If you're picking a bundler for a fresh project, our Vite vs Turbopack vs Rspack comparison covers the ecosystem trade-offs.
Tailwind v4 vs v3: should you upgrade?
Greenfield projects: yes, start on v4. No question. The CSS-native config is genuinely better, the Vite plugin is fast, and you'll never need to migrate later. Bootstrap with Vite, the @tailwindcss/vite plugin, and skip PostCSS entirely. Deploy to Vercel or Netlify and the build pipeline picks it up automatically.
Existing v3 projects under active development: upgrade, but budget half a day. The codemod gets you 80% there. The remaining 20% is hunting down visual regressions from the border/ring/shadow scale changes, fixing @apply in scoped styles, and updating any custom plugins (the plugin API changed — most popular plugins like @tailwindcss/forms have v4-compatible versions, but check before you upgrade).
Existing v3 projects in maintenance mode: wait. v3 isn't going anywhere yet, and the upgrade pays back over time, not immediately. If you're shipping bug fixes and not features, the migration cost outweighs the benefit. Revisit in six months when the plugin ecosystem has fully caught up.
Component library authors: this is the painful case. If you ship a Tailwind-based component library (shadcn-style), supporting both v3 and v4 consumers means duplicate configs and careful class-name choices. Most libraries are picking a side. Check your dependencies before you upgrade — if your design system is on v3, you're waiting.
The verdict
Tailwind v4 is the upgrade Tailwind needed. The JS config was always a wart — it existed because CSS custom properties weren't expressive enough in 2019, and now they are. Moving to CSS-native config aligns Tailwind with the platform rather than fighting it.
Start new projects on v4. Migrate active v3 projects within the next quarter. Skip the migration on anything in maintenance mode. The Vite plugin alone is worth the move if you've been frustrated by PostCSS build times — pair it with Biome and Vitest and your dev loop will feel noticeably tighter.
One last thing the docs bury: monitor your CSS bundle size after upgrading. The new engine is more aggressive about JIT generation, and if you're using a lot of dynamic class composition, you can end up shipping more utilities than you expect. Sentry performance monitoring or DebugBear will surface the regression before your users do.