Tailwind CSS v4 Features Deep Dive: Performance, Configuration, and Migration Guide
35 milliseconds. That’s how long Tailwind v3 takes for an incremental build—seems fast, right? But in v4, the same operation takes just 192 microseconds. Not milliseconds—microseconds. That’s nearly 200 times faster.
Honestly, when I first saw this number, I was skeptical. Marketing claims are easy to make. But after running the migration myself and watching HMR drop from 340ms to 12ms, I realized—Tailwind is serious this time.
This is the real impact of the Rust-based Oxide engine rewrite. Not just a “feels a bit faster” psychological effect, but the kind of experience where you change a padding value and the page refreshes before you can blink. Tailwind v4 isn’t just about swapping engines—it completely transforms the configuration approach (moving from JS to CSS), installation workflow (zero config), and utility syntax (opacity modifiers, Container Queries, 3D Transforms…).
In this article, I’ll break down these changes clearly and provide you with a migration checklist you can follow step by step.
The Oxide Engine—Why is v4 So Fast?
Have you ever experienced this scenario: you change a color value, save, and then stare at the browser for two or three seconds before seeing the change? In large projects, Tailwind v3’s incremental builds can definitely cause some anxiety—especially when you’re racing against a deadline.
v4’s Oxide engine addresses this pain point directly. The Tailwind team didn’t just patch up the old JS engine—they completely rewrote the compiler in Rust from scratch. This was a bold decision, but it brought tangible benefits.
How Much Performance Actually Improved?
The official team ran benchmarks using the Catalyst project, and the data is striking:
| Test Scenario | v3.4 | v4.0 | Improvement |
|---|---|---|---|
| Full build | 378ms | 100ms | 3.78x |
| Incremental (CSS changed) | 44ms | 5ms | 8.8x |
| Incremental (no CSS change) | 35ms | 192µs | 182x |
That last number is the most impressive. When you only change HTML structure without adding new CSS classes, v4’s build time drops to microseconds—a delay you can’t even perceive.
Here’s another dataset from a production project (500+ component codebase):
| Metric | v3.4 | v4.0 | Change |
|---|---|---|---|
| Cold build | 12.3s | 1.8s | 85% faster |
| Dev server startup | 4.2s | 0.8s | 81% faster |
| HMR update | 340ms | 12ms | 96% faster |
| Production CSS size | 48KB | 31KB | 35% smaller |
| Memory usage | 180MB | 45MB | 75% less |
Dropping HMR from 340ms to 12ms makes a huge impact on developer experience. Before, changing styles meant waiting for a “noticeable pause”—now it’s essentially instant.
What Did Oxide Do Right?
The core of the Oxide engine is Rust + Lightning CSS. Everyone knows Rust’s performance advantages, but the architectural changes are even more important:
Unified toolchain. In the v3 era, Tailwind relied on the PostCSS ecosystem, requiring additional configuration for autoprefixer, cssnano, and other tools. v4 built all of these in, depending only on the Lightning CSS package. Fewer middlemen naturally means faster performance.
Intelligent content detection. Previously, you had to manually configure the content array in tailwind.config.js to tell Tailwind which files to scan for class names. v4 reads .gitignore and the module graph directly to automatically discover files that need scanning. Less configuration, and faster too.
Native CSS features. v4 uses real @layer rules, @property custom properties, color-mix() color functions, and other modern CSS features. These are natively supported by browsers and don’t need compile-time transformations.
Honestly, you don’t need to fully understand these technical details. What matters is the result: faster builds, less configuration, better developer experience.
CSS-first Configuration—Say Goodbye to tailwind.config.js
This is the biggest change in v4, and also the part with the highest migration cost.
Previously we wrote configuration in tailwind.config.js. Now we need to move it to CSS files using the @theme directive. Honestly, I wasn’t used to it at first—after writing JS configs for years, suddenly writing CSS variables felt awkward. But after using it for a while, I realized it actually fits better with CSS’s way of thinking.
Configuration Migration: Before & After
Let’s look at the simplest example. Customizing a primary color:
// v3: tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: '#3b82f6',
},
},
},
}
In v4, this all goes into CSS:
/* v4: app.css */
@import "tailwindcss";
@theme {
--color-primary: #3b82f6;
}
You might notice the variable name changed from primary to --color-primary. This is because v4 has strict naming prefix rules.
Variable Naming Rules Reference Table
| v3 Config Item | v4 CSS Variable Prefix | Example |
|---|---|---|
| colors | —color-* | —color-primary: #3b82f6 |
| spacing | —spacing-* | —spacing-128: 32rem |
| fontSize | —text-* | —text-xs: 0.75rem |
| fontFamily | —font-* | —font-sans: “Inter” |
| borderRadius | —radius-* | —radius-lg: 0.5rem |
| screens | —breakpoint-* | —breakpoint-md: 768px |
| boxShadow | —shadow-* | —shadow-card: 0 4px 12px rgba(0,0,0,0.1) |
| animation | —animate-* | —animate-spin: spin 1s linear infinite |
This prefix system feels tedious at first, but it has a benefit: you don’t have to guess variable names. Want to know how to write a font size variable? It definitely starts with --text-.
A More Complex Configuration Example
Let’s look at a more complete example. Say your v3 config looks like this:
// v3: tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: {
light: '#f0f9ff',
DEFAULT: '#0ea5e9',
dark: '#0369a1',
},
},
fontFamily: {
display: ['Cal Sans', 'sans-serif'],
},
animation: {
'fade-in': 'fadeIn 0.5s ease-out',
},
},
},
plugins: [
require('@tailwindcss/typography'),
],
}
Migrating to v4:
/* v4: app.css */
@import "tailwindcss";
@theme {
/* Colors */
--color-brand-light: #f0f9ff;
--color-brand: #0ea5e9;
--color-brand-dark: #0369a1;
/* Fonts */
--font-display: "Cal Sans", sans-serif;
/* Animations */
--animate-fade-in: fadeIn 0.5s ease-out;
}
/* Plugins */
@plugin "@tailwindcss/typography";
Notice a few changes:
- Color nesting flattened. v3 supported nested objects
brand.light, v4 requires--color-brand-light - Plugins use @plugin. No need to
requirein JS anymore - DEFAULT suffix removed. The default color for
brandis now just--color-brand
Dark Mode Configuration
v3’s dark mode configuration looked like this:
// v3
module.exports = {
darkMode: 'class', // or 'media'
}
v4 defaults to the media strategy (follows system preference). If you want to change to class, add one line in CSS:
/* v4 */
@import "tailwindcss";
@variant dark (&:where(.dark, .dark *));
This code means: when an element or its parent has the .dark class, apply dark mode styles.
Content Detection Configuration
Previously you had to manually specify which files to scan:
// v3
module.exports = {
content: [
'./src/**/*.{js,ts,jsx,tsx}',
'./public/index.html',
],
}
v4 automatically reads .gitignore, ignores files you’ve excluded, then scans all relevant files in your project. If your project structure is unusual, you can manually add paths with @source:
/* v4 */
@import "tailwindcss";
@source "../node_modules/my-ui-lib";
The biggest benefit of CSS-first configuration: all style-related configuration is in one CSS file, no jumping back and forth between JS and CSS. The downside is the learning curve, especially for developers used to JS configuration.
Installation and Integration—A Zero-Config New Experience
Anyone who has installed Tailwind v3 knows the workflow: install three packages, create config file, configure PostCSS, write content array… While not overly complex, you have to go through it for every new project.
v4 simplifies this process to the extreme.
Minimal Installation
If you use Vite, you only need two steps:
# 1. Install
npm install tailwindcss @tailwindcss/vite
# 2. Add one line to vite.config.js
import tailwindcss from '@tailwindcss/vite'
export default {
plugins: [tailwindcss()],
}
Then add one import line to your CSS file:
/* app.css or index.css */
@import "tailwindcss";
That’s it. No tailwind.config.js, no postcss.config.js, no content array. Works out of the box.
Three Integration Methods
Vite Integration (recommended):
npm install tailwindcss @tailwindcss/vite
// vite.config.js
import tailwindcss from '@tailwindcss/vite'
export default {
plugins: [tailwindcss()],
}
PostCSS Integration:
npm install tailwindcss @tailwindcss/postcss
// postcss.config.js
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
CLI Usage:
npx tailwindcss -i input.css -o output.css --watch
For most modern projects, the Vite integration is sufficient. The PostCSS approach is suitable for migrating older projects, while CLI is for scenarios without a build pipeline.
How Automatic Content Detection Works
v4 doesn’t require you to configure a content array—it automatically discovers files that need scanning. How?
It reads your .gitignore file to exclude directories that don’t need scanning (node_modules, dist, etc.), then searches for Tailwind class names in the remaining files.
This mechanism has a prerequisite: your project should have a standard Node.js structure. If you put template files in unusual locations, like a folder outside the project root, you need to specify manually:
@import "tailwindcss";
@source "../templates"; /* Manually add scan path */
Automatic detection has another benefit: no config updates for new files. Previously, every time you created a new component file, you had to update the content array in tailwind.config.js (unless you used wildcards **/*, but some project structures don’t support that). Now you don’t have to worry about it—new files just work.
Breaking Changes and Migration Checklist
This is the core of migration. Don’t panic—most changes follow patterns, and there’s an official automated migration tool.
Opacity Modifier Changes
This is the most impactful change. In v3, setting background opacity required:
<!-- v3 -->
<div class="bg-blue-500 bg-opacity-50">...</div>
v4 integrates opacity into the color value:
<!-- v4 -->
<div class="bg-blue-500/50">...</div>
Not just background colors—all color-related properties support this syntax:
<!-- Text color opacity -->
<p class="text-gray-900/75">...</p>
<!-- Border color opacity -->
<div class="border-red-500/30">...</div>
What about class names like bg-opacity-*? Just remove them. v4 no longer supports them.
Renamed Utility Classes
v4 renamed some utility classes, mainly for simplification and semantic adjustment:
| v3 Class Name | v4 Class Name | Notes |
|---|---|---|
flex-grow | grow | Simplified naming |
flex-grow-* | grow-* | Simplified naming |
flex-shrink | shrink | Simplified naming |
flex-shrink-* | shrink-* | Simplified naming |
overflow-ellipsis | text-ellipsis | Category adjustment |
decoration-slice | box-decoration-slice | Category adjustment |
shadow-sm | shadow-xs | Size renaming |
shadow | shadow-sm | Size renaming |
rounded-sm | rounded-xs | Size renaming |
rounded | rounded-sm | Size renaming |
outline-none | outline-hidden | Semantic change |
Note the shadow and rounded changes: the original shadow became shadow-sm, and the original shadow-sm became shadow-xs. This is a semantic adjustment to make size naming more consistent.
Default Value Changes
Several default values changed, which may cause style inconsistencies:
Border default color: v3 used gray-200, v4 changed to currentColor. This means border colors will follow text color.
<!-- v3: border is gray -->
<div class="border text-blue-500">Border is gray-200</div>
<!-- v4: border follows text color -->
<div class="border text-blue-500">Border is blue-500</div>
Ring default value: v3 default width 3px, color blue-500; v4 default width 1px, color currentColor.
<!-- v3: ring is blue 3px -->
<button class="ring">...</button>
<!-- v4: ring is currentColor 1px -->
<button class="ring">...</button>
<!-- For v3 effect -->
<button class="ring-3 ring-blue-500">...</button>
Complete Migration Checklist
Execute in order:
-
Upgrade dependencies
npm install tailwindcss@latest @tailwindcss/vite@latest -
Run automatic migration tool
npx @tailwindcss/upgradeThis tool automatically converts most syntax, including
@tailwinddirectives, opacity class names, renamed utility classes, etc. -
Convert CSS entry file
/* v3 */ @tailwind base; @tailwind components; @tailwind utilities; /* v4 */ @import "tailwindcss"; -
Migrate config file: Move configuration from
tailwind.config.jsto the CSS@themeblock. -
Update plugins: Change JS plugins to
@pluginsyntax.@plugin "@tailwindcss/typography"; -
Check opacity class names: Globally search for
bg-opacity,text-opacity,border-opacity, replace with new syntax. -
Check renamed class names: Focus on
shadow-*,rounded-*,flex-grow-*,flex-shrink-*. -
Check default value changes: Pay special attention to border colors and ring styles.
-
Test visual regression: Run your visual tests to ensure no missed changes.
The automatic migration tool handles 80% of the work, but the remaining 20% still requires manual review. Especially those default value changes—the tool can’t change your expectations, only you can adjust them.
New Features—Container Queries, 3D Transforms, and More
Besides performance and configuration improvements, v4 also brings some practical new features.
Built-in Container Queries Support
Previously you needed a plugin for Container Queries—now it’s natively supported:
<!-- Define container -->
<div class="@container">
<!-- Responsive based on container width -->
<div class="@md:grid-cols-2 @lg:grid-cols-3">
...
</div>
</div>
@container is equivalent to container-type: inline-size, and @md: is the container query breakpoint. The syntax is similar to responsive breakpoints md:, just with @ prepended.
This is particularly suitable for component libraries—component styles can adapt based on parent container width rather than screen width.
3D Transform Utilities
v4 added a set of 3D transform utilities:
<!-- 3D perspective -->
<div class="perspective-distant">
<!-- X-axis rotation -->
<div class="rotate-x-45">...</div>
</div>
<!-- Y-axis rotation -->
<div class="rotate-y-12">...</div>
<!-- Z-axis scale -->
<div class="scale-z-150">...</div>
Available classes include rotate-x-*, rotate-y-*, rotate-z-*, scale-z-*, perspective-*, translate-z-*, etc. Making card flip effects, 3D menus, and similar interactions is much easier now.
@starting-style Variant
This works with CSS native @starting-style feature, for styles when elements first render:
<!-- Element fades from transparent to opaque on appearance -->
<div class="starting:opacity-0 opacity-100 transition-opacity">
...
</div>
No JavaScript needed for entrance animations. Previously you had to use custom animations like animate-fade-in, now one class name does it.
not-* Variant
Supports CSS :not() pseudo-class:
<!-- All child elements except the last -->
<li class="not-last:mb-4">...</li>
<!-- All buttons without disabled state -->
<button class="not-disabled:opacity-100">...</button>
Previously you had to use the inverse approach like last:mb-0, now not-* is more intuitive.
Extended Gradient API
v4’s gradients are more powerful:
<!-- Conic gradient -->
<div class="bg-conic/from-red-500 via-yellow-500 to-blue-500">...</div>
<!-- Radial gradient -->
<div class="bg-radial from-white to-transparent">...</div>
<!-- Gradient interpolation mode -->
<div class="bg-linear-to-r from-blue-500 to-purple-500 via-oklch">...</div>
via-oklch is the new interpolation mode, making gradient transitions more natural (especially during color space conversions).
Conclusion
After all this, should you upgrade?
If your project is still in active development, my recommendation is: yes. Dropping HMR from hundreds of milliseconds to about ten milliseconds saves you a lot of waiting time every day. Plus reduced CSS size and lower memory usage—it’s especially worthwhile for large projects.
Migration cost is mainly in configuration conversion and class name changes. Fortunately, there’s the automatic migration tool npx @tailwindcss/upgrade that handles most repetitive work. I tried it—a medium-sized project (200+ components) takes about half a day to migrate, including testing.
For new projects, use v4 directly. Zero-config installation, automatic content detection, CSS-first configuration—these changes make Tailwind even better to use, there’s no reason not to.
One thing to note about browser compatibility: v4 requires Safari 16.4+, Chrome 111+, Firefox 128+. If your project needs to support older browsers, you might want to wait.
Action items:
- New projects: use v4 directly
- Existing projects: use
npx @tailwindcss/upgradefor automatic migration - Focus on checking border default color and ring default value changes
- Check the official upgrade documentation if you encounter issues
Tailwind CSS v4 Migration Guide
Complete step-by-step instructions for upgrading from Tailwind v3 to v4
⏱️ Estimated time: 30 min
- 1
Step1: Upgrade dependency packages
Run the npm install command to upgrade to the latest version:
```bash
npm install tailwindcss@latest @tailwindcss/vite@latest
```
If using PostCSS integration, install:
```bash
npm install tailwindcss@latest @tailwindcss/postcss@latest
``` - 2
Step2: Run automatic migration tool
Tailwind provides a one-click migration tool:
```bash
npx @tailwindcss/upgrade
```
This tool automatically handles:
- @tailwind directive conversion to @import "tailwindcss"
- Opacity class names from bg-opacity-* to /50 syntax
- Renamed utility classes (shadow-sm → shadow-xs, etc.)
- Config file conversion to CSS @theme format - 3
Step3: Convert CSS entry file
Replace the original three @tailwind directives with one @import:
```css
/* Remove these */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Replace with */
@import "tailwindcss";
``` - 4
Step4: Migrate custom config to CSS
Move theme configuration from tailwind.config.js to CSS file:
```css
@import "tailwindcss";
@theme {
/* Color configuration */
--color-brand: #0ea5e9;
/* Font configuration */
--font-display: "Cal Sans", sans-serif;
/* Animation configuration */
--animate-fade-in: fadeIn 0.5s ease-out;
}
```
Note the variable naming rules: colors → --color-*, fontSize → --text-* - 5
Step5: Update plugin import method
Use @plugin directive for JS plugins:
```css
/* Old method: in tailwind.config.js */
// plugins: [require('@tailwindcss/typography')]
/* New method: in CSS file */
@plugin "@tailwindcss/typography";
``` - 6
Step6: Check default value changes
Focus on two default value changes:
- **border color**: Changed from gray-200 to currentColor
- If you need the original gray border, explicitly add border-gray-200
- **ring default value**: Changed from 3px blue-500 to 1px currentColor
- If you need the original blue 3px ring, use ring-3 ring-blue-500 - 7
Step7: Test and fix styles
Run development server to check for visual changes:
```bash
npm run dev
```
Focus on checking:
- Opacity-related styles are working correctly
- shadow and rounded sizes match expectations
- Border colors match design specs
- Run project's visual regression tests (if available)
FAQ
What's the biggest difference between Tailwind CSS v4 and v3?
How long does it take to migrate to Tailwind v4?
What are the browser compatibility requirements for v4?
What can the automatic migration tool handle?
Does v4 still need to configure the content array?
What if styles are inconsistent after upgrading from v3?
What new features in v4 are worth noting?
12 min read · Published on: Mar 25, 2026 · Modified on: Mar 25, 2026
Related Posts
Tailwind v4 + Vite: Complete Setup Template in 5 Minutes
Tailwind v4 + Vite: Complete Setup Template in 5 Minutes
VPS Selection for Website Building: Configuration, Routes, and Control Panels
VPS Selection for Website Building: Configuration, Routes, and Control Panels
From Copilot to Antigravity: Mastering the Agent-First Development Paradigm

Comments
Sign in with GitHub to leave a comment