Tailwind CSS 4 is not a normal major release. It’s a near-complete rewrite of the framework’s internals, and it ships with a philosophy shift that catches a lot of developers completely off guard. The config file you’ve been tweaking for years? Gone by default. The three @tailwind directives you paste into every project? Replaced by a single import. The PostCSS setup you copied from the docs? Needs updating.
The good news: builds are genuinely faster, the CSS output is cleaner, and the new configuration model is actually more ergonomic once you internalize it. The bad news: there are enough silent behavior changes that a naive upgrade will leave you with a broken UI and no obvious error messages to help you find out why.
Official repo: https://github.com/tailwindlabs/tailwindcss
This article is the migration playbook I wish existed when I first ran npm install tailwindcss@4 on a real project.
What Is the Oxide Engine?
The original Tailwind CSS was built on top of PostCSS and written entirely in JavaScript. It worked well, but scanning your entire source tree, resolving utilities, and generating a CSS bundle is inherently slow in JS — especially at scale.
Oxide replaces the scanning and transformation core with a Rust-based engine. The CSS parsing and transformation layer is powered by Lightning CSS, which handles vendor prefixing, syntax lowering, and CSS nesting natively. This means Autoprefixer is no longer a required peer dependency — Oxide handles that internally.
The performance numbers are real. On a medium-sized project (50k+ lines of source), cold build times drop from several seconds to under a second. Incremental rebuilds in watch mode are near-instant.
But performance is almost a side story. The bigger deal is what the rewrite enabled — a fundamentally different approach to configuration and content detection.
The Three Biggest Conceptual Shifts
Before you touch a single file, you need to understand these three changes at a conceptual level. If you skip this section and go straight to the commands, you’ll hit walls you can’t debug.
1. CSS-first configuration
In v3, your design tokens lived in tailwind.config.js. Colors, spacing, fonts, breakpoints — all JavaScript. In v4, the configuration model moves into CSS using a new @theme directive. Your CSS file becomes the single source of truth.
/* This is now valid Tailwind config */
@import "tailwindcss";
@theme {
--color-primary: oklch(55% 0.2 250);
--font-sans: "Inter", sans-serif;
--spacing-18: 4.5rem;
}
The tailwind.config.js file isn’t deleted — it still works via a @config directive — but it’s no longer the default, and the framework is clearly moving away from it.
2. Automatic content detection
In v3, you had to manually specify a content array telling Tailwind where to scan for class names. Forget to add a directory and your classes silently don’t generate. In v4, content detection is automatic. The engine scans everything in your project that isn’t explicitly ignored (node_modules, dist, etc.).
This sounds great and mostly is. The gotcha is that automatic detection can miss files in unusual locations, and you won’t always get a warning about it. We’ll cover that.
3. CSS import replaces the three directives
Out with this:
@tailwind base;
@tailwind components;
@tailwind utilities;
In with this:
@import "tailwindcss";
Simple. But if your build pipeline is watching for the old directives — some editor plugins and Prettier plugins do — expect friction.
Full Breaking Changes Inventory
Here’s everything that changed that can silently break your UI. Go through this list against your actual codebase.
Default ring width
In v3, ring (without a size) generated a 3px ring. In v4, the default is 1px. If you use bare ring anywhere and you’re seeing hairline outlines instead of chunky ones, this is why.
Fix: Replace bare ring with ring-3 wherever you relied on the old default.
Shadow, blur, and rounded defaults
Several utilities had their "bare" defaults adjusted. shadow now maps to a smaller shadow than before. blur similarly. rounded is now 0.25rem as before, but rounded-sm changed from 0.125rem to 0.125rem — that one stayed, but rounded-md and above shifted slightly.
The safest move: run a visual regression test on your UI after upgrading. These are the changes that break things without breaking the build.
Color palette changes
Tailwind v4 ships a new default color palette. The hues are similar but not identical to v3. If you’re using classes like bg-blue-500 and expecting a specific hex value in your design system, verify the output. The new palette uses OKLCH under the hood, which means the colors are perceptually more uniform but numerically different.
Opacity modifier replaces opacity utilities
v3 had standalone opacity utilities like opacity-50. v4 still supports these, but the preferred pattern is now using the /<opacity> modifier:
<!-- v3 style, still works -->
<div class="opacity-50">...</div>
<!-- v4 style, preferred -->
<div class="bg-black/50 text-white/75">...</div>
This is additive rather than breaking, but if your linting or custom tooling chokes on the slash syntax, update it.
Removed and renamed utilities
Several deprecated v3 utilities are gone in v4:
overflow-ellipsis→text-ellipsisoverflow-clip→text-clipflex-grow-*→grow-*flex-shrink-*→shrink-*decoration-clone/decoration-slice→box-decoration-clone/box-decoration-slice
None of these will throw a build error. The class just won’t exist and the style won’t apply. Grep your codebase for the old names.
JIT is the only mode
v3 still had a concept of "JIT mode" being a toggle. v4 removes the distinction — everything runs JIT-style, always. This has no practical breaking effect for most projects since JIT has been the default for years, but any tooling or documentation referencing mode: 'jit' in the config is irrelevant.
PostCSS plugin import path
The PostCSS plugin moved:
// v3
plugins: {
tailwindcss: {},
}
// v4
plugins: {
'@tailwindcss/postcss': {},
}
This is the most common first error people hit. Your build will fail outright if you forget this one, which at least makes it easy to find.
Migration Playbook
Step 0: Run the official codemod first
Tailwind ships an upgrade tool that handles the mechanical parts of the migration:
npx @tailwindcss/upgrade
Run this on a clean branch. It will:
- Update your package.json dependencies
- Rewrite your CSS entry point
- Attempt to migrate your
tailwind.config.jsto CSS@themeblocks - Update PostCSS config
The codemod is good but not perfect. It handles straightforward configs cleanly. Complex configs with custom plugins, safelist, or deep extend structures will require manual follow-up. Treat the codemod output as a starting point, not a finished migration.
Step 1: Update dependencies
npm install tailwindcss@latest @tailwindcss/postcss@latest
# If you use Vite:
npm install @tailwindcss/vite@latest
# Remove autoprefixer if it was only there for Tailwind:
npm uninstall autoprefixer
Step 2: Update PostCSS config
// postcss.config.js
export default {
plugins: {
'@tailwindcss/postcss': {},
// autoprefixer is handled internally now — remove it
},
};
If you’re on Vite, skip PostCSS config entirely and use the Vite plugin:
// vite.config.js
import tailwindcss from '@tailwindcss/vite';
export default {
plugins: [
tailwindcss(),
],
};
Step 3: Update your CSS entry point
/* Before */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* After */
@import "tailwindcss";
If you had custom base styles or component classes between the directives, move them into @layer base and @layer components blocks below the import.
Step 4: Migrate your config
Option A — CSS-first (recommended for new or simple configs):
The codemod should have generated @theme blocks in your CSS. Review them. A typical migration looks like:
@import "tailwindcss";
@theme {
/* Colors */
--color-brand: oklch(55% 0.18 260);
--color-brand-dark: oklch(40% 0.18 260);
/* Typography */
--font-display: "Cabinet Grotesk", sans-serif;
/* Custom spacing */
--spacing-18: 4.5rem;
--spacing-22: 5.5rem;
}
Option B — Keep tailwind.config.js with a compatibility shim:
If your config is complex and you can’t migrate it all at once, use the @config directive to point v4 at your existing v3 config file:
@import "tailwindcss";
@config "./tailwind.config.js";
This enables a compatibility layer. Most v3 config options are understood, but some edge cases aren’t. Test thoroughly. Treat this as a temporary bridge, not a permanent solution.
Step 5: Fix content detection issues
If you have utility classes in files that aren’t being picked up, add explicit sources:
@import "tailwindcss";
@source "../templates/**/*.html";
@source "../../shared/components/**/*.tsx";
This is the v4 replacement for the content array in the old config.
Step 6: Fix the renamed/removed utilities
Run these greps on your source:
# Find old opacity utilities that should become modifiers
rg 'overflow-ellipsis|overflow-clip' --type html --type tsx --type jsx
# Find removed flex utilities
rg 'flex-grow-|flex-shrink-' --type html --type tsx --type jsx
# Find old decoration classes
rg 'decoration-clone|decoration-slice' --type html --type tsx --type jsx
For each match, apply the rename from the breaking changes list above.
Step 7: Fix the ring default
rg '\bring\b(?!-[a-z])' --type html --type tsx --type jsx
Anything matching bare ring (not followed by a dash) needs to become ring-3 to preserve v3 behavior.
Step 8: Verify shadow and border-radius visuals
No good automated fix here. Open your app in a browser and do a visual pass of:
- Cards, panels, and any element using
shadoworshadow-sm - Rounded corners, especially
rounded-sm,rounded-md - Focus rings on form inputs
Take screenshots before and after if you have a staging environment. If you have a Storybook or similar component explorer, run through it systematically.
Gotchas That Will Waste Your Time
The codemod breaks monorepo setups. If you have multiple tailwind.config.js files across packages, the codemod may only find and migrate one of them, or may get confused about which CSS entry point belongs to which package. Run it once per package with the working directory set explicitly.
CSS @import order matters more now. In v4, your @import "tailwindcss" needs to come before any @theme or @layer blocks that depend on it. If you have a CSS architecture that splits files and assembles them with PostCSS imports, ensure the Tailwind import lands first.
Custom plugins using the v3 plugin API will break. The plugin() helper still works but the internal API it exposes has changed. If your plugins use addBase, addComponents, or matchUtilities, test them individually. The Tailwind docs have a v4 plugin migration section worth reading in full.
TypeScript configs for tailwind.config.ts are no longer supported. The v3 community had a pattern of writing configs in TypeScript with ts-node. v4 doesn’t recognize .ts config files. Convert to JS before migrating, or go full CSS-first.
Dark mode config syntax changed. If you had darkMode: 'class' in your v3 config, the equivalent in v4 is:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
The media strategy is the default and requires no config. Only change this if you’re using class-based dark mode.
Safelist is handled differently. The safelist array in v3 config doesn’t have a direct equivalent in v4’s CSS config. Instead, use a source file that explicitly references the classes you need to safelist:
/* safelist.css — committed to your repo and included as a @source */
.bg-red-500 .bg-green-500 .bg-blue-500
Then add @source "./safelist.css"; in your entry point.
Production Checklist Before Cutting Over
- All build pipelines produce output without errors
- Run
npx tailwindcss --input src/input.css --output dist/output.cssand inspect the output manually for unexpected changes - Visual regression pass on all major UI states (light mode, dark mode, mobile, desktop)
- Focus styles on interactive elements look correct
- Custom fonts and colors render as expected
- Form inputs, buttons, and modals with shadow/ring styles verified
- Any third-party Tailwind plugins (daisyUI, headlessui, etc.) confirmed compatible with v4
- CI/CD pipeline updated to use the new PostCSS plugin reference
Should You Migrate Now?
If you’re starting a new project: yes, start with v4. The CSS-first config is cleaner, the performance is better, and you won’t carry v3 mental models into it.
If you have an existing production app: wait for your next planned maintenance window, allocate a full day for the migration and visual review, and do it on a feature branch with thorough QA before merging. Don’t treat this as a quick dependency bump — it isn’t.
The Tailwind team has committed to supporting v3 with security patches, so there’s no emergency. Migrate when you have the capacity to do it properly.
The Oxide engine is genuinely a better foundation. The migration friction is real but bounded. Once it’s done, you won’t miss the old setup.