Tailwind CSS 4 Migration Guide: Oxide Engine, CSS-First Configuration, and New Features
Tailwind CSS 4 is a ground-up rewrite with a new Rust-based engine called Oxide that delivers dramatically faster builds and introduces CSS-first configuration — replacing the JavaScript tailwind.config.js with CSS @theme directives. A Tailwind CSS 4 migration requires understanding these fundamental changes before upgrading your project, because the differences are not cosmetic — they touch how you configure the framework, how plugins work, and how your build pipeline is wired together. Therefore, this guide covers the new features, the migration process, and the gotchas that will save you hours of debugging.
The Oxide Engine: Why It’s So Much Faster
Tailwind 3’s JavaScript-based engine scanned your source files, generated CSS, and ran through PostCSS — all in Node.js. The Oxide engine rewrites the critical path in Rust, with parallel file scanning, zero-copy parsing, and incremental compilation. Moreover, the engine detects changes at a granular level, so editing a single class regenerates only the affected utilities instead of the entire stylesheet. The practical effect is that hot module replacement feels instant, even on large component libraries where the old engine introduced a noticeable pause.
For large projects, the Tailwind team’s published benchmarks show full builds dropping from several hundred milliseconds to tens of milliseconds, with incremental rebuilds an order of magnitude faster again. Additionally, Oxide reduces memory usage substantially because it no longer holds a JavaScript object representing every possible utility class in memory. In contrast, Tailwind 3 materialized that full universe of classes up front, which is why memory grew with the size of your config. Another underappreciated change is that content detection now works automatically — Oxide infers which files to scan from your project structure, so the content array you used to maintain by hand is gone in most setups.
/* tailwind.css — CSS-first configuration replaces tailwind.config.js */
@import "tailwindcss";
/* Define your theme with @theme — replaces module.exports config */
@theme {
--color-primary: #3b82f6;
--color-primary-dark: #1d4ed8;
--color-secondary: #8b5cf6;
--color-surface: #0f172a;
--color-surface-light: #1e293b;
--font-sans: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--breakpoint-xs: 475px; /* Custom breakpoint */
--animate-fade-in: fade-in 0.3s ease-out;
--animate-slide-up: slide-up 0.4s ease-out;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-up {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
CSS-First Configuration: Goodbye tailwind.config.js
The biggest conceptual change is moving configuration from JavaScript to CSS. Your tailwind.config.js is replaced by @theme blocks in your CSS file. Custom colors become CSS custom properties with a --color- prefix, fonts use --font-, spacing uses --spacing-, and so on. Consequently, your configuration lives alongside your other CSS, and you can use CSS features like @media and calc() directly in your theme. Because these tokens are real custom properties, they are also available at runtime in the browser — you can read or override them with JavaScript, which was awkward when the source of truth lived in a Node config file.
Plugins also change. JavaScript plugins that added utilities via addUtilities() are replaced by CSS @utility directives. This is simpler for most use cases but means complex programmatic plugins need rethinking. Furthermore, the @apply directive still works but is discouraged — Tailwind 4 improves the developer experience enough that inline utilities are practical for more situations. One important nuance: because tokens become real CSS variables, a value like --color-primary automatically generates the matching utility classes (bg-primary, text-primary, border-primary), so you no longer enumerate utilities separately from values.
/* Custom utilities in CSS — replaces JavaScript plugins */
@utility text-balance {
text-wrap: balance;
}
@utility glass {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.15);
}
/* Custom variants */
@variant hocus (&:hover, &:focus-visible);
@variant group-hocus (group:hover &, group:focus-visible &);
/* Usage: <div class="glass hocus:bg-white/20 p-6"> */
Migration from Tailwind v3: Step by Step
Start by running the official upgrade tool: npx @tailwindcss/upgrade. It converts your config to CSS-first format, updates deprecated class names, and adjusts your PostCSS configuration. However, the automated tool doesn’t catch everything — review the output carefully and run it on a clean branch so the diff is easy to inspect. The tool requires Node 20 or newer, and it works best when your project is already on the latest Tailwind 3 release, since it migrates from a known-good baseline rather than from years of accumulated deprecations.
Key breaking changes to watch for: the @tailwind base/components/utilities directives are replaced by a single @import "tailwindcss". Color opacity modifiers like bg-blue-500/50 still work, but the theme() function in CSS gives way to standard CSS custom properties. Additionally, some default values have shifted — for example, the default border color is now currentColor rather than gray, and several utilities renamed their scales (shadow-sm became shadow-xs, and the bare shadow is now shadow-sm). As a result, you should visually audit your pages after migration rather than trusting that a green build means a correct render.
# Step 1: Run the automated upgrade tool
npx @tailwindcss/upgrade
# Step 2: Install new dependencies
npm install tailwindcss@latest @tailwindcss/vite
# Step 3: Update vite.config.js
# Remove postcss-related tailwind plugins
# Add @tailwindcss/vite plugin
# Step 4: Update your CSS entry point
# Before (v3):
# @tailwind base;
# @tailwind components;
# @tailwind utilities;
#
# After (v4):
# @import "tailwindcss";
# @theme { ... }
# Step 5: Delete tailwind.config.js (after migrating to @theme)
# Step 6: Visual regression testing
// vite.config.js — Tailwind 4 with Vite
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
react(),
tailwindcss(), // Replaces PostCSS plugin
],
});
Keeping a JavaScript Config During Transition
Not every project can adopt CSS-first configuration overnight. Teams with design tokens generated by a build script, or shared config consumed by Storybook and other tooling, often need to keep their JavaScript config alive temporarily. Fortunately, Tailwind 4 provides an escape hatch: the @config directive loads a legacy tailwind.config.js file so you can migrate incrementally. This lets you move most of your setup to @theme while keeping a few stubborn plugins or programmatically generated values in JavaScript until you are ready to rewrite them.
/* Load a legacy config during the transition period */
@import "tailwindcss";
@config "../tailwind.config.js";
/* New CSS-first tokens can coexist with the legacy config */
@theme {
--color-brand: oklch(0.62 0.19 250);
}
Practically, a phased migration looks like this: first switch the build pipeline and the CSS entry point, verify nothing breaks, then move color and spacing tokens into @theme, and finally retire the JavaScript file once every consumer has a CSS equivalent. Splitting the work this way keeps each pull request small and reviewable, which matters more on a large shared codebase than getting to the finish line in one commit.
New Features Worth Adopting
Container queries are now built-in with the @container variant. Instead of responsive breakpoints based on viewport width, components respond to their container’s width. This makes truly reusable components that work in sidebars, modals, and full-width layouts without media query hacks. For example, a product card can switch from horizontal to vertical layout based on whether it sits in a three-column grid or a narrow sidebar — the same markup adapts wherever you drop it.
Beyond containers, the new release leans heavily on modern CSS color. Tailwind 4 ships its default palette in the oklch() color space, which produces more vivid, perceptually uniform colors on wide-gamut displays. The field-sizing: content utility auto-sizes textareas and inputs to fit their content, 3D transforms are first-class with rotate-x-*, rotate-y-*, and perspective-* utilities, and a new @starting-style support makes entry animations possible without JavaScript. Furthermore, color mixing with color-mix() lets you create dynamic tints and shades without defining every shade in your theme.
/* Container queries adapt a component to its parent, not the viewport */
.card-wrapper {
container-type: inline-size;
}
/* Layout flips based on the wrapper's width, wherever it is placed */
@container (min-width: 24rem) {
.product-card { display: flex; gap: 1rem; }
}
/* Or directly with Tailwind utilities in markup: */
/* <div class="@container">
<article class="flex flex-col @md:flex-row gap-4">...</article>
</div> */
When NOT to Migrate Yet: Trade-offs to Weigh
Tailwind 4 is not a free upgrade for every project. The most important constraint is browser support: the new engine depends on modern CSS features like cascade layers, @property, and color-mix(), which means it targets recent versions of Safari, Chrome, and Firefox. If you must support older browsers — for instance, an enterprise audience locked to legacy WebViews — staying on Tailwind 3 is the pragmatic choice rather than shipping broken styles. Test against your real analytics, not assumptions, before committing.
There are other reasons to hold off. Plugin compatibility varies: official plugins like @tailwindcss/typography and @tailwindcss/forms have v4 versions, but some third-party plugins have not been updated, and the @apply directive behaves differently across CSS module boundaries. The @apply directive also no longer supports certain opacity modifiers the way it did in v3, so heavily abstracted component CSS may need rework. Additionally, any JavaScript that reads tailwind.config.js programmatically — design-token exporters, documentation generators, visual test harnesses — needs updating since the file no longer exists by default. If your team has a deep web of such tooling, budget time for it rather than discovering the breakage in CI. For deeper performance context, see the Core Web Vitals optimization guide.
Related Reading:
- Core Web Vitals Optimization Guide
- Next.js 15 Server Components
- Progressive Web Apps Offline-First Guide
Resources:
In conclusion, a Tailwind CSS 4 migration delivers significant performance improvements through the Oxide engine and simplifies configuration with CSS-first @theme directives. The migration is straightforward for most projects — run the upgrade tool, update your build config, and visually test your pages. New features like container queries, oklch() colors, and built-in 3D transforms expand what’s possible with utility-first CSS. Just confirm your browser support targets and plugin dependencies first, start migration on a feature branch, and test thoroughly before merging.