Tailwind CSS 4 is a ground-up rewrite. Not a major version with some breaking changes and new utilities — a completely new engine written in Rust with a fundamentally different configuration model. If you've been using Tailwind v3, the way you configure and extend the framework has changed. The utility classes you write in your HTML are mostly the same, but everything behind the scenes is different.
The Big Changes
A New Engine in Rust
Tailwind v3 used a JavaScript-based engine with PostCSS. v4 replaces this with a new engine written in Rust (called Oxide) that handles parsing, compilation, and optimization in a single pass. The result is build speeds that are 5-10x faster on large projects.
You won't interact with the engine directly, but you'll notice it: dev server startup is near-instant, hot reloads are faster, and production builds complete in seconds instead of tens of seconds.
CSS-First Configuration
This is the biggest conceptual change. In v3, you configured Tailwind through tailwind.config.js — a JavaScript file where you defined your theme, plugins, and content paths:
// v3 — tailwind.config.js (the old way)
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
brand: "#E21B1B",
},
fontFamily: {
sans: ["Ubuntu", "sans-serif"],
},
},
},
plugins: [require("@tailwindcss/typography")],
};In v4, configuration lives in your CSS file using @theme blocks. No JavaScript config file needed:
/* v4 — app/global.css (the new way) */
@import "tailwindcss";
@theme {
--color-brand: #e21b1b;
--font-sans: "Ubuntu", sans-serif;
--font-serif: "Source Serif 4", serif;
--font-mono: "JetBrains Mono", monospace;
}That's it. The @theme block defines CSS custom properties that Tailwind reads to generate utilities. --color-brand gives you bg-brand, text-brand, border-brand, etc. --font-sans gives you font-sans. The naming convention maps directly to utility classes.
No More tailwind.config.js
For most projects, you don't need a JavaScript config file at all. The CSS-first approach covers:
- Custom colors:
--color-* - Font families:
--font-* - Spacing scale:
--spacing-* - Breakpoints:
--breakpoint-* - Border radius:
--radius-* - Shadows:
--shadow-* - Animations:
--animate-*
If you need programmatic configuration (dynamic themes, conditional plugins), you can still use a config file, but it's no longer the default path.
Automatic Content Detection
v3 required you to specify content paths so Tailwind knew which files to scan for class names:
// v3
content: ['./src/**/*.{js,ts,jsx,tsx}']v4 automatically detects your source files. It uses heuristics based on your project structure and framework to find the right files. For Next.js projects, it knows to scan the app/ directory. For most setups, zero configuration for content detection.
If you need to override detection, use @source in your CSS:
@source "../components/**/*.tsx";
@source "../lib/**/*.ts";New Features
@custom-variant
Define custom variants directly in CSS:
@custom-variant dark (&:where(.dark, .dark *));
@custom-variant hocus (&:hover, &:focus);
@custom-variant scrolled (&:where(.scrolled *));Now you can use dark:bg-gray-900, hocus:underline, or scrolled:shadow-lg in your markup. The &:where() syntax controls specificity — using :where() keeps the specificity at zero, preventing cascade issues.
Container Queries Built In
v3 needed a plugin for container queries. v4 includes them natively:
<div class="@container">
<div class="@sm:grid-cols-2 @lg:grid-cols-3">
<!-- Responds to container width, not viewport -->
</div>
</div>The @container class marks the query container. @sm:, @md:, @lg: prefixes work like responsive prefixes but are relative to the container's width.
3D Transforms
New utilities for 3D transformations:
<div class="perspective-500">
<div class="rotate-y-12 translate-z-8 transform-3d">3D element</div>
</div>Field Sizing
The field-sizing utility lets form elements auto-size based on content:
<textarea class="field-sizing-content" placeholder="Type here..."></textarea>The textarea grows as the user types. No JavaScript needed.
Not Variant
Target elements that don't match a condition:
<div class="not-last:border-b">Item with bottom border (except the last one)</div>
<div class="not-hover:opacity-50">Dimmed unless hovered</div>Descendant Variant
Style all descendants matching a selector:
<div class="**:data-[slo=description]:text-sm">
<!-- All descendants with data-slot="description" get text-sm -->
</div>Migration from v3 to v4
Step 1: Update Packages
pnpm install tailwindcss@latest @tailwindcss/postcss@latestThe PostCSS plugin changed from tailwindcss to @tailwindcss/postcss:
// postcss.config.cjs
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};Step 2: Update Your CSS Entry Point
Replace the old directives with a single import:
/* v3 */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* v4 */
@import "tailwindcss";Step 3: Move Theme Configuration to CSS
Convert your tailwind.config.js theme values to @theme blocks:
@import "tailwindcss";
@theme {
/* Colors */
--color-brand: #e21b1b;
--color-surface: #ffffff;
--color-surface-dark: #111111;
/* Fonts — note: --font-sans, not --font-family-sans */
--font-sans: "Ubuntu", sans-serif;
--font-serif: "Source Serif 4", serif;
--font-mono: "JetBrains Mono", monospace;
/* Custom spacing */
--spacing-18: 4.5rem;
--spacing-22: 5.5rem;
/* Breakpoints */
--breakpoint-xs: 475px;
/* Animations */
--animate-slide-up: slide-up 0.3s ease-out;
}
@keyframes slide-up {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}Step 4: Handle Breaking Changes in Class Names
A few utilities were renamed or changed:
| v3 | v4 |
|---|---|
bg-opacity-50 | bg-black/50 (modifier syntax, already available in v3) |
decoration-clone | box-decoration-clone |
decoration-slice | box-decoration-slice |
flex-grow | grow (alias, both work) |
flex-shrink | shrink (alias, both work) |
overflow-ellipsis | text-ellipsis |
The shadow, blur, and ring utilities that used to have default values (e.g., shadow with no size) may need explicit sizes in v4 (shadow-sm, shadow-md).
Step 5: Update Plugins
First-party plugins are now CSS-based:
/* v3: require('@tailwindcss/typography') in config */
/* v4: import in CSS */
@plugin "@tailwindcss/typography";Third-party plugins that use the JavaScript plugin API still work but may need updates from their maintainers.
Step 6: Delete tailwind.config.js
Once everything is migrated to CSS, delete the config file. Fewer files, less indirection.
Real Example: Before and After
Here's a complete global.css for a Next.js project in v4:
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-brand: #e21b1b;
--color-muted: #6b7280;
--font-sans: "Ubuntu", sans-serif;
--font-serif: "Source Serif 4", serif;
--font-mono: "JetBrains Mono", monospace;
--animate-fade-in: fade-in 0.2s ease-out;
--animate-slide-up: slide-up 0.3s ease-out;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-up {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
/* Custom base styles */
@layer base {
body {
@apply bg-white text-gray-900 dark:bg-black dark:text-gray-100;
}
}Configuration, customization, and custom CSS — all in one file. No JavaScript config, no separate plugin requires, no content path arrays. This is what Tailwind CSS 4 is about: less configuration, more CSS, faster builds.
Is It Worth Upgrading?
If you're starting a new project, use v4. There's no reason to start with v3 in 2026.
For existing projects, the migration is moderate effort. Most utility classes haven't changed, so your templates are fine. The work is moving your config to CSS and updating your PostCSS setup. For small projects it's an hour of work. For large projects with custom plugins and extensive theme customization, set aside a day.
The performance improvements alone justify the upgrade for large codebases. But the real benefit is the simpler mental model: your configuration is CSS, your utilities are CSS, your customizations are CSS. One language, one file, one source of truth.