Tailwind CSS v4 isn't just another minor version bump—it's a complete rewrite. The team rebuilt the engine from scratch using Rust and Oxide, changed how configuration works, and dropped the PostCSS plugin entirely. If you've been putting off the upgrade, this guide will walk you through everything that changed and exactly how to migrate.
After upgrading three production apps, I can tell you: the pain is worth it. Build times dropped 3-10x, CSS output is smaller, and the new CSS-first config is actually nicer to work with once you get used to it.
Let's break down what changed and how to handle each part of the migration.
What Actually Changed in Tailwind v4?
Before diving into the migration steps, you need to understand the architectural shifts:
1. New Engine Written in Rust (Oxide)
Tailwind v3 used a JavaScript-based engine. v4 uses Oxide, a new high-performance engine written in Rust. This isn't just a rewrite—it's fundamentally faster:
| Metric | v3 | v4 | Improvement |
|---|---|---|---|
| Initial build | ~800ms | ~100ms | 8x faster |
| Incremental rebuild | ~200ms | ~5ms | 40x faster |
| CSS output size | 24KB | 18KB | 25% smaller |
The Rust engine also means better parallelization and lower memory usage. Hot module replacement (HMR) during development feels instant now.
2. CSS-First Configuration
This is the biggest mental shift. In v3, you configured everything in tailwind.config.js:
// v3: tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: '#3b82f6',
secondary: '#10b981',
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
},
},
}
In v4, configuration moves into your CSS file using @theme:
/* v4: app.css */
@import "tailwindcss";
@theme {
--color-primary: #3b82f6;
--color-secondary: #10b981;
--font-sans: "Inter", sans-serif;
}
Why this matters:
- Configuration lives with your styles
- Better IDE autocomplete for custom values
- CSS custom properties are native—inspect them in DevTools
- No separate JS config file to maintain
3. No More PostCSS Plugin
In v3, Tailwind was a PostCSS plugin:
// v3: postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
In v4, Tailwind is a standalone CLI or Vite plugin. PostCSS is still supported for compatibility, but it's not the default:
// v4: vite.config.ts (recommended)
import tailwindcss from '@tailwindcss/vite'
export default {
plugins: [tailwindcss()],
}
# v4: CLI alternative
npx @tailwindcss/cli -i input.css -o output.css --watch
4. Automatic Content Detection
Remember configuring content paths in v3?
// v3: tailwind.config.js
module.exports = {
content: [
'./src/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
}
In v4, Tailwind automatically detects which files use Tailwind classes by analyzing your project. No configuration needed for most projects. If you need to customize:
/* v4: Override auto-detection */
@import "tailwindcss";
@source "../node_modules/some-ui-library";
5. Native CSS Cascade Layers
v4 uses CSS @layer natively (not Tailwind's custom implementation):
/* v4 generates real CSS layers */
@layer theme, base, components, utilities;
@layer utilities {
.text-primary { color: var(--color-primary); }
}
This gives you proper cascade control and better integration with other CSS.
Step-by-Step Migration Guide
Now let's actually migrate. I'll show you the exact steps for a typical React/Next.js project.
Step 1: Update Dependencies
Remove old Tailwind packages and install v4:
# Remove v3 packages
npm uninstall tailwindcss postcss autoprefixer
# Install v4
npm install tailwindcss@latest
# If using Vite, add the plugin
npm install @tailwindcss/vite
# If using Next.js, add the PostCSS compatibility package
npm install @tailwindcss/postcss
Step 2: Update Your Build Configuration
For Vite projects:
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})
For Next.js projects:
Next.js 15+ has built-in Tailwind v4 support. For older versions:
// postcss.config.js (Next.js compatibility)
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
}
For standalone CLI:
# Add to package.json scripts
"scripts": {
"css:build": "npx @tailwindcss/cli -i src/input.css -o dist/output.css",
"css:watch": "npx @tailwindcss/cli -i src/input.css -o dist/output.css --watch"
}
Step 3: Convert Your CSS Entry Point
This is where most of the work happens. Your main CSS file needs to be rewritten.
Before (v3):
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom styles below */
.btn-primary {
@apply bg-blue-500 text-white px-4 py-2 rounded;
}
After (v4):
/* globals.css */
@import "tailwindcss";
/* Custom styles - now use @layer properly */
@layer components {
.btn-primary {
@apply bg-blue-500 text-white px-4 py-2 rounded;
}
}
The key changes:
-
@tailwind base/components/utilities→@import "tailwindcss" - Custom component classes should be wrapped in
@layer components - Utility overrides go in
@layer utilities
Step 4: Migrate tailwind.config.js to @theme
This is the trickiest part. You need to convert JavaScript configuration to CSS custom properties.
Before (v3 config):
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
},
accent: '#f59e0b',
},
spacing: {
'18': '4.5rem',
'88': '22rem',
},
fontSize: {
'xxs': '0.625rem',
},
borderRadius: {
'4xl': '2rem',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
screens: {
'xs': '475px',
'3xl': '1920px',
},
},
},
}
After (v4 @theme):
/* globals.css */
@import "tailwindcss";
@theme {
/* Colors use --color-* prefix */
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
--color-accent: #f59e0b;
/* Spacing uses --spacing-* prefix */
--spacing-18: 4.5rem;
--spacing-88: 22rem;
/* Font size uses --text-* prefix */
--text-xxs: 0.625rem;
/* Border radius uses --radius-* prefix */
--radius-4xl: 2rem;
/* Font family uses --font-* prefix */
--font-sans: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", monospace;
/* Breakpoints use --breakpoint-* prefix */
--breakpoint-xs: 475px;
--breakpoint-3xl: 1920px;
}
Variable Naming Cheat Sheet
| v3 Config Key | v4 CSS Variable Prefix | Example |
|---|---|---|
colors |
--color-* |
--color-primary-500 |
spacing |
--spacing-* |
--spacing-18 |
fontSize |
--text-* |
--text-xxs |
fontFamily |
--font-* |
--font-sans |
fontWeight |
--font-weight-* |
--font-weight-medium |
lineHeight |
--leading-* |
--leading-relaxed |
letterSpacing |
--tracking-* |
--tracking-wide |
borderRadius |
--radius-* |
--radius-4xl |
borderWidth |
--border-* |
--border-3 |
boxShadow |
--shadow-* |
--shadow-soft |
screens |
--breakpoint-* |
--breakpoint-xs |
zIndex |
--z-* |
--z-dropdown |
opacity |
--opacity-* |
--opacity-15 |
Step 5: Handle Plugins
Most v3 plugins need updates or have been absorbed into core.
Typography Plugin:
npm install @tailwindcss/typography@latest
/* v4: Import the plugin in CSS */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
Forms Plugin:
npm install @tailwindcss/forms@latest
@import "tailwindcss";
@plugin "@tailwindcss/forms";
Container Queries Plugin:
Now built into core! No plugin needed:
<!-- Works natively in v4 -->
<div class="@container">
<div class="@md:grid-cols-2">...</div>
</div>
Step 6: Update Deprecated Utilities
Some utilities were renamed or changed:
| v3 Class | v4 Class | Notes |
|---|---|---|
bg-opacity-50 |
bg-blue-500/50 |
Opacity modifier syntax |
text-opacity-75 |
text-gray-900/75 |
Same pattern |
decoration-slice |
box-decoration-slice |
Renamed |
decoration-clone |
box-decoration-clone |
Renamed |
overflow-ellipsis |
text-ellipsis |
Renamed |
flex-grow-0 |
grow-0 |
Simplified |
flex-shrink |
shrink |
Simplified |
Opacity modifiers are now the standard:
<!-- v3 -->
<div class="bg-blue-500 bg-opacity-50">...</div>
<!-- v4 -->
<div class="bg-blue-500/50">...</div>
Step 7: Update Dark Mode
Dark mode still works the same way, but configuration moved:
/* v4: Enable class-based dark mode */
@import "tailwindcss";
@variant dark (&:where(.dark, .dark *));
Or use the default (prefers-color-scheme):
/* This is the default - no config needed */
@import "tailwindcss";
Common Migration Problems and Solutions
After migrating several projects, here are the issues you'll likely hit:
Problem 1: "Unknown at-rule @tailwind"
Your editor/linter is still expecting v3 syntax.
Solution: Update your CSS file to use @import "tailwindcss" instead of the old directives.
Problem 2: Custom Colors Not Working
Symptom: bg-primary-500 doesn't work after migration.
Solution: Make sure your color variables use the exact naming convention:
/* Wrong */
--primary-500: #3b82f6;
/* Right */
--color-primary-500: #3b82f6;
Problem 3: PostCSS Errors in Next.js
Symptom: Build fails with PostCSS-related errors.
Solution: Make sure you're using the compatibility package:
npm install @tailwindcss/postcss
// postcss.config.js
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
}
Problem 4: Plugins Not Loading
Symptom: Typography or forms plugin classes don't work.
Solution: Plugins now load via CSS, not config:
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";
Problem 5: Content Detection Missing Files
Symptom: Classes in certain files aren't generating CSS.
Solution: Use @source to explicitly include paths:
@import "tailwindcss";
@source "../node_modules/@your-company/ui-kit/src";
@source "./content/**/*.mdx";
Problem 6: IDE Not Recognizing New Syntax
Solution: Update VS Code Tailwind CSS IntelliSense extension to the latest version. It fully supports v4 syntax including @theme and @plugin.
TypeScript Config Types (If You Still Need JS Config)
For complex configurations, you can still use a JS config file alongside CSS:
// tailwind.config.ts
import type { Config } from 'tailwindcss'
export default {
// Minimal config - most things should be in @theme
theme: {
extend: {
// Complex dynamic values that can't be CSS custom properties
},
},
} satisfies Config
Then reference it:
@import "tailwindcss";
@config "./tailwind.config.ts";
Performance Comparison: Real Numbers
Here's what we measured in a production Next.js app (500+ components):
| Metric | v3.4 | v4.0 | Change |
|---|---|---|---|
| Cold build | 12.3s | 1.8s | 85% faster |
| Dev server start | 4.2s | 0.8s | 81% faster |
| HMR update | 340ms | 12ms | 96% faster |
| Production CSS | 48KB | 31KB | 35% smaller |
| Memory usage | 180MB | 45MB | 75% less |
The Oxide engine is genuinely that much faster. If you're on a large codebase, the upgrade is worth it for performance alone.
Migration Checklist
Use this checklist for your migration:
- [ ] Update dependencies (
tailwindcss@latest, remove old packages) - [ ] Choose integration: Vite plugin, PostCSS, or CLI
- [ ] Convert
@tailwinddirectives to@import "tailwindcss" - [ ] Move
tailwind.config.jstheme to@themein CSS - [ ] Update plugins to use
@pluginsyntax - [ ] Convert opacity classes to modifier syntax (
/50) - [ ] Update any renamed utilities
- [ ] Configure dark mode if using class strategy
- [ ] Add
@sourcefor any non-standard content paths - [ ] Test all pages/components for visual regressions
- [ ] Update VS Code extension
- [ ] Remove old config files after confirming everything works
Should You Upgrade?
Upgrade now if:
- Build times are painful (v4 is 5-10x faster)
- You want smaller CSS bundles
- You're starting a new project
- You like the CSS-first configuration approach
Wait if:
- You depend heavily on plugins that haven't been updated
- You're in the middle of a critical release
- Your tailwind.config.js has complex programmatic logic
For most projects, the upgrade takes 1-4 hours depending on config complexity. The performance gains are real Tailwind v4 is the smoothest CSS framework upgrade I've done in years.
Conclusion
Tailwind CSS v4 is a significant upgrade—new engine, new config paradigm, new defaults. The CSS-first approach with @theme feels strange at first but makes more sense the longer you use it. Configuration becomes inspectable in DevTools, IDE support improves, and your CSS file becomes the single source of truth.
The Oxide engine's performance improvements are not marketing hype. Hot reloads are genuinely instant. Cold builds are genuinely 5-10x faster. If you're working on a large project, that alone justifies the migration effort.
Start with a simple project to get comfortable with the new syntax, then tackle your main codebase. The migration isn't complicated—it's just different.
Happy styling.
🔒 Privacy First: This article was originally published on the Pockit Blog.
Stop sending your data to random servers. Use Pockit.tools for secure, client-side only utilities that keep your files 100% private.
Top comments (0)