Tailwind CSS v4: What's New and How to Migrate
Everything you need to know about Tailwind CSS v4 — the new CSS-first configuration, lightning-fast engine, new utilities, container queries, and step-by-step migration from v3.
What Changed in Tailwind v4?
Tailwind CSS v4 is a ground-up rewrite with a new engine that's up to 10x faster. The biggest change: configuration moves from tailwind.config.js to your CSS file.
Key changes at a glance:
| Feature | v3 | v4 |
|---------|----|----|
| Configuration | tailwind.config.js | CSS @theme directive |
| Build speed | Fast | 10x faster (full builds in ~5ms) |
| Engine | PostCSS plugin | Standalone (Oxide engine) |
| Container queries | Plugin needed | Built-in |
| 3D transforms | Not available | Built-in |
| @apply | Worked | Still works, less needed |
| Color opacity | bg-red-500/50 | Same + P3 wide gamut colors |
Getting Started
New Project
# Install
npm install tailwindcss @tailwindcss/vite
# For Next.js
npm install tailwindcss @tailwindcss/postcssCSS Setup
/* app.css — This IS your config now */
@import "tailwindcss";That's it. No tailwind.config.js, no @tailwind base/components/utilities. One import.
Vite Configuration
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});Next.js / PostCSS
// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};CSS-First Configuration
The biggest paradigm shift — your theme lives in CSS, not JavaScript.
v3 (Old Way)
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: "#3b82f6",
surface: "#1e293b",
},
fontFamily: {
heading: ["Cal Sans", "sans-serif"],
},
spacing: {
18: "4.5rem",
},
},
},
};v4 (New Way)
@import "tailwindcss";
@theme {
--color-brand: #3b82f6;
--color-surface: #1e293b;
--font-heading: "Cal Sans", sans-serif;
--spacing-18: 4.5rem;
}Use it exactly the same in HTML:
<div class="bg-brand text-surface font-heading p-18">
Themed content
</div>How @theme Variables Map to Classes
@theme {
/* --color-{name} → bg-{name}, text-{name}, border-{name} */
--color-primary: #6366f1;
--color-primary-light: #818cf8;
/* --font-{name} → font-{name} */
--font-mono: "JetBrains Mono", monospace;
/* --spacing-{name} → p-{name}, m-{name}, gap-{name}, w-{name}, h-{name} */
--spacing-sidebar: 280px;
/* --radius-{name} → rounded-{name} */
--radius-card: 12px;
/* --shadow-{name} → shadow-{name} */
--shadow-soft: 0 2px 8px rgba(0, 0, 0, 0.08);
/* --breakpoint-{name} → sm:, md:, custom: responsive prefixes */
--breakpoint-xs: 480px;
/* --animate-{name} → animate-{name} */
--animate-slide-in: slide-in 0.3s ease-out;
}
@keyframes slide-in {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}New Utilities in v4
Container Queries
Style elements based on their parent container's size, not the viewport.
<div class="@container">
<div class="flex flex-col @md:flex-row @lg:grid @lg:grid-cols-3">
<div class="@sm:text-lg @md:text-xl">
Responsive to container, not viewport
</div>
</div>
</div>Named containers:
<div class="@container/sidebar">
<nav class="@sm/sidebar:flex @md/sidebar:flex-col">
Responds to sidebar container size
</nav>
</div>3D Transforms
<div class="perspective-500">
<div class="rotate-x-12 rotate-y-6 translate-z-4">
3D transformed element
</div>
</div>New Color Utilities
<!-- P3 wide gamut colors -->
<div class="bg-red-500">Standard sRGB</div>
<!-- Color mixing -->
<div class="bg-blue-500/50">50% opacity</div>
<!-- currentColor -->
<div class="text-blue-500 border-current">Border matches text</div>Field Sizing
<!-- Textarea that grows with content -->
<textarea class="field-sizing-content"></textarea>New Variant: inert
<div class="inert:opacity-50 inert:pointer-events-none" inert>
Disabled section
</div>not-* Variant
<!-- Style all children except last -->
<div class="not-last:mb-4">...</div>
<!-- Style when NOT hovered -->
<button class="not-hover:opacity-75">Button</button>Dark Mode
@import "tailwindcss";
@theme {
--color-bg: #ffffff;
--color-text: #0f172a;
--color-surface: #f8fafc;
}
/* Dark mode overrides using @variant or media query */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0f172a;
--color-text: #f8fafc;
--color-surface: #1e293b;
}
}Or use the built-in dark: variant as before:
<div class="bg-white dark:bg-slate-900 text-black dark:text-white">
Adaptive content
</div>Migration from v3 to v4
Step 1: Update Dependencies
npm install tailwindcss@latest @tailwindcss/postcss@latest
npm uninstall autoprefixer # No longer needed, built into v4Step 2: Update PostCSS Config
// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
// Remove: tailwindcss, autoprefixer
},
};Step 3: Update CSS Entry Point
/* Before (v3) */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* After (v4) */
@import "tailwindcss";Step 4: Move Config to CSS
Move your tailwind.config.js theme into @theme:
@import "tailwindcss";
@theme {
/* Move colors */
--color-brand: #your-color;
/* Move fonts */
--font-sans: "Inter", sans-serif;
/* Move any custom spacing, radius, etc. */
}Step 5: Handle Breaking Changes
<!-- Renamed utilities -->
<!-- v3: blur-sm → v4: blur-xs (blur-sm is now a different value) -->
<!-- v3: rounded-sm → v4: rounded-xs -->
<!-- v3: shadow-sm → v4: shadow-xs -->
<!-- v3: shadow → v4: shadow-sm -->
<!-- Ring width -->
<!-- v3: ring → v4: ring-3 (ring now defaults to 1px) -->
<!-- Border color -->
<!-- v3: border (gray default) → v4: border (currentColor default) -->
<!-- Fix: use border-gray-200 explicitly -->Step 6: Content Detection
v4 auto-detects your content files — no content array needed. It scans your project automatically (respects .gitignore).
To add extra sources:
@import "tailwindcss";
@source "../node_modules/my-ui-lib/src/**/*.tsx";Plugins in v4
Official Plugins
npm install @tailwindcss/typography @tailwindcss/forms@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";Custom Plugin (CSS-based)
@import "tailwindcss";
@utility glass {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
@utility text-gradient {
background: linear-gradient(135deg, var(--color-brand), var(--color-accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}<div class="glass rounded-xl p-6">
<h1 class="text-gradient text-4xl font-bold">Glassmorphism</h1>
</div>Performance
v4's Oxide engine delivers dramatic speed improvements:
v3 v4
Full build: 300ms ~5ms
Incremental: ~100ms <1ms
This is because v4:
- Is written in Rust (via the Oxide engine)
- Scans files in parallel
- Has zero PostCSS overhead when using the Vite plugin
Practical Examples
Responsive Card Grid
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div class="group rounded-xl border border-gray-200 bg-white p-6
shadow-xs transition-all hover:shadow-md hover:-translate-y-1
dark:border-gray-800 dark:bg-gray-900">
<h3 class="text-lg font-semibold text-gray-900 group-hover:text-brand
dark:text-white">
Card Title
</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Card description text goes here.
</p>
</div>
</div>Container Query Sidebar
<aside class="@container w-64 lg:w-80">
<nav class="flex flex-col gap-1">
<a class="flex items-center gap-3 rounded-lg px-3 py-2
text-sm @[200px]:text-base hover:bg-gray-100">
<span class="hidden @[200px]:inline">Dashboard</span>
<span class="@[200px]:hidden">D</span>
</a>
</nav>
</aside>Summary
Tailwind v4 is faster, simpler, and more powerful. The CSS-first approach means less tooling, fewer config files, and a more natural workflow. If you're starting a new project, use v4. If you're on v3, migrate when you have time — the breaking changes are manageable and the performance gains are significant.