Tailwind CSS v4 — What's New and How to Upgrade from v3
Everything that changed in Tailwind CSS v4: CSS-first configuration, the @theme directive, new color system, container queries, 3D transforms, and a practical v3 migration path.
Tailwind v4 dropped the JavaScript config file. That single change ripples through everything — how you define your design tokens, how you import the framework, how your build pipeline works, and how you think about customization. If you've been writing tailwind.config.js files for years, v4 requires you to unlearn some habits.
The payoff is real though. Builds are dramatically faster (the new Oxide engine is written in Rust), the configuration lives in CSS where it arguably always should have been, and you get features like container queries and 3D transforms out of the box.
Here's what actually changed, what the upgrade looks like, and where things get tricky.
The Big Shift: CSS-First Configuration
In v3, your design system lived in JavaScript:
// tailwind.config.js (v3) — gone in v4
module.exports = {
theme: {
extend: {
colors: {
brand: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a5a',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
spacing: {
'18': '4.5rem',
'88': '22rem',
},
borderRadius: {
'4xl': '2rem',
},
},
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms'),
],
}
In v4, all of that moves to your CSS file using the @theme directive:
/ app.css (v4) — this IS your config now /
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";
@theme {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5a;
--font-sans: "Inter", "system-ui", sans-serif;
--font-mono: "JetBrains Mono", monospace;
--spacing-18: 4.5rem;
--spacing-88: 22rem;
--radius-4xl: 2rem;
}
No more JavaScript file. No more require(). No more module.exports. Your design tokens are CSS custom properties, which means they're inspectable in DevTools, overridable in media queries, and composable with other CSS tools.
The @theme Directive in Detail
The @theme block is where you define every design token. Tailwind maps these CSS variables to utility classes automatically.
@theme {
/ Colors → generates bg-primary, text-primary, border-primary, etc. /
--color-primary: #6366f1;
--color-secondary: #ec4899;
--color-surface: #f8fafc;
--color-surface-dark: #1e293b;
/ Typography /
--font-display: "Cal Sans", sans-serif;
--text-xs: 0.75rem;
--text-xs--line-height: 1rem;
/ Shadows — note the nested line-height syntax /
--shadow-soft: 0 2px 8px rgb(0 0 0 / 0.06);
--shadow-hard: 0 4px 12px rgb(0 0 0 / 0.15);
/ Custom breakpoints /
--breakpoint-xs: 30rem;
--breakpoint-3xl: 120rem;
/ Animation /
--animate-fade-in: fade-in 0.3s ease-out;
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
Every --color- variable becomes a color utility. Every --spacing- becomes a spacing utility. The naming convention is the mapping — no configuration object needed.
Overriding Default Theme Values
Want to wipe out Tailwind's default color palette and use only yours?
@theme {
--color-: initial; / Clears ALL default colors */
--color-black: #000;
--color-white: #fff;
--color-brand: #6366f1;
--color-muted: #94a3b8;
--color-danger: #ef4444;
}
The --color-*: initial syntax resets the entire namespace. This replaces v3's theme (non-extend) overrides.
Import Changes
v3 used three directives:
/ v3 /
@tailwind base;
@tailwind components;
@tailwind utilities;
v4 uses a single import:
/ v4 /
@import "tailwindcss";
That one line pulls in the base reset, all utilities, and the theme. If you need to add custom base styles or component classes, use @layer:
@import "tailwindcss";
@layer base {
html {
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
}
}
@layer components {
.btn-primary {
@apply rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white
hover:bg-primary/90 focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-primary;
}
}
New Color System
v4 moves to OKLCH as the default color space. The entire palette was regenerated for perceptual uniformity — meaning blue-500 and red-500 actually look like they have the same visual weight now, which was never quite true in v3's HSL-derived palette.
You won't notice this in most cases. The class names are identical (bg-blue-500, text-red-700). But if you've hardcoded hex values that matched v3's palette, they'll be slightly different.
/ The default palette uses OKLCH internally /
/ You can still define custom colors in any format /
@theme {
--color-brand: oklch(0.65 0.2 260); / OKLCH /
--color-accent: #f59e0b; / Hex /
--color-surface: rgb(248 250 252); / RGB /
--color-overlay: hsl(220 14% 10% / 0.8); / HSL with alpha /
}
Container Queries — Built In
This was a plugin in v3. Now it's native. Container queries let components respond to their parent's size instead of the viewport — which is what you actually want most of the time for reusable components.
<!-- Mark an element as a container -->
<div class="@container">
<div class="grid grid-cols-1 @md:grid-cols-2 @lg:grid-cols-3 gap-4">
<div class="p-4 @sm:p-6">
<h3 class="text-base @md:text-lg font-semibold">Card Title</h3>
<p class="hidden @sm:block text-muted">
Only shows when the container is wide enough
</p>
</div>
</div>
</div>
The @ prefix differentiates container queries from viewport breakpoints. @md means "when my container is at least md-width," not "when the viewport is."
You can also name containers:
<div class="@container/sidebar">
<nav class="@lg/sidebar:flex-row flex flex-col gap-2">
<!-- Responds to sidebar container width -->
</nav>
</div>
3D Transforms
v4 adds first-class 3D transform utilities. No more fighting with arbitrary values for perspective and rotation.
<!-- Card flip effect -->
<div class="group perspective-1000">
<div class="relative transition-transform duration-500
transform-3d group-hover:rotate-y-180">
<!-- Front -->
<div class="absolute inset-0 backface-hidden rounded-xl bg-white p-6 shadow-lg">
<h3>Front of card</h3>
</div>
<!-- Back -->
<div class="absolute inset-0 backface-hidden rotate-y-180
rounded-xl bg-indigo-600 p-6 text-white shadow-lg">
<h3>Back of card</h3>
</div>
</div>
</div>
Key new utilities:
| Utility | What it does |
|---|---|
perspective-* | Sets CSS perspective on parent |
rotate-x-, rotate-y- | 3D rotation around X/Y axis |
translate-z-* | Movement along Z axis |
transform-3d | Enables transform-style: preserve-3d |
backface-hidden | Hides back face of rotated elements |
scale-z-* | Scale along Z axis |
Build Performance: The Oxide Engine
v4's compiler is written in Rust (the Oxide engine) and processes CSS significantly faster than v3's JavaScript-based engine. On a large project with thousands of utility usages, rebuild times can drop from hundreds of milliseconds to single-digit milliseconds.
The practical impact: hot reload in development is nearly instant. Full production builds that took 3-4 seconds now complete in under a second.
PostCSS Setup Changes
v3:
// postcss.config.js (v3)
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
v4:
// postcss.config.js (v4)
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
}
The package name changed. Autoprefixer is built in — you don't need it as a separate dependency anymore.
Upgrading from v3 — Step by Step
1. Update Dependencies
npm install tailwindcss@4 @tailwindcss/postcss@4
npm uninstall autoprefixer # Built into v4
# If you use plugins:
npm install @tailwindcss/typography@4 @tailwindcss/forms@4
2. Update PostCSS Config
// postcss.config.js
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
}
3. Migrate Your CSS Entry Point
Replace the three directives with a single import, then move your config:
/ Before (v3) /
@tailwind base;
@tailwind components;
@tailwind utilities;
/ After (v4) /
@import "tailwindcss";
4. Move tailwind.config.js to @theme
This is the manual part. Take your theme.extend values and convert them to CSS variables:
// v3 config (delete after migration)
module.exports = {
theme: {
extend: {
colors: {
brand: '#6366f1',
surface: { light: '#f8fafc', dark: '#0f172a' },
},
spacing: { '18': '4.5rem' },
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
},
},
}
Becomes:
@theme {
--color-brand: #6366f1;
--color-surface-light: #f8fafc;
--color-surface-dark: #0f172a;
--spacing-18: 4.5rem;
--font-sans: "Inter", sans-serif;
}
5. Update Plugin Imports
/ v3: plugins were in tailwind.config.js /
/ v4: plugins go in CSS /
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";
6. Use the Automated Upgrade Tool
Tailwind provides a codemod to handle most of this automatically:
npx @tailwindcss/upgrade
This scans your project, migrates the config, updates CSS files, and adjusts class names for any breaking changes. It handles maybe 80% of the migration. The remaining 20% is usually custom plugin migrations and edge cases with arbitrary values.
7. Check for Breaking Class Name Changes
Some utilities were renamed or removed:
| v3 | v4 | Notes |
|---|---|---|
decoration-clone | box-decoration-clone | Full name now |
decoration-slice | box-decoration-slice | Full name now |
flex-grow | grow | Shortened (both still work) |
flex-shrink | shrink | Shortened (both still work) |
overflow-ellipsis | text-ellipsis | Renamed |
bg-opacity-* | bg-black/50 | Opacity modifier syntax preferred |
bg-opacity-, text-opacity-, etc.) still work but the slash modifier syntax (bg-black/50) is strongly preferred.
When NOT to Upgrade
Hold off on v4 if:
- You depend on v3-only plugins that haven't been updated. Check your plugin list first.
- You have a massive custom plugin with
addUtilities/addComponents— the plugin API changed, and your plugin needs rewriting. - Your build pipeline does unusual things with the Tailwind config (reading it programmatically, generating it dynamically). The JS config is gone, so those patterns break.
- You're mid-sprint on a deadline. The migration is straightforward but not zero-effort. Budget a day for a large project.
Real-World Migration Notes
After migrating several projects at codeup.dev, a few things stood out:
- The codemod misses
@applyin component libraries. If you have a shared UI library using@applyheavily, review those files manually after the migration.
- Dark mode configuration changed. If you were using
darkMode: 'class'in v3, you'll need to adjust. v4 uses the CSSprefers-color-schememedia query by default, and you enable class-based dark mode with a variant:
@variant dark (&:where(.dark, .dark *));
- Content paths are automatic. v4 detects your source files automatically — no more
content: ['./src/*/.{js,ts,jsx,tsx}']configuration. It scans everything in your project directory.
- JIT is the only mode. This was already the default in v3, but v4 removes the classic engine entirely. Every build is JIT.
Quick Reference: v3 vs v4
| Feature | v3 | v4 |
|---|---|---|
| Config format | JavaScript (tailwind.config.js) | CSS (@theme directive) |
| Import syntax | @tailwind base/components/utilities | @import "tailwindcss" |
| Plugin loading | require() in JS config | @plugin in CSS |
| Color space | HSL-derived | OKLCH |
| Container queries | Plugin (@tailwindcss/container-queries) | Built in (@container) |
| 3D transforms | Arbitrary values only | First-class utilities |
| Build engine | JavaScript | Rust (Oxide) |
| Autoprefixer | Separate dependency | Built in |
| Content detection | Manual paths in config | Automatic |
| PostCSS package | tailwindcss | @tailwindcss/postcss |