Tailwind CSS v4: What's New, What Changed, and How to Migrate

Tailwind CSS v4 is the most significant release since v1. The engine was rewritten in Rust (via the oxide engine), the JavaScript config file is replaced by CSS-native configuration, and the full build pipeline is redesigned for near-instant HMR. If you've been using Tailwind v3, here's what changed and exactly how to migrate.

The Big Changes at a Glance

  • Rust engine: 5–10× faster full builds, sub-millisecond HMR in most projects
  • CSS-first configuration: no more tailwind.config.js — customize via CSS @theme directive
  • No PostCSS config required: single @import "tailwindcss" in your CSS is enough
  • Automatic content detection: no content array to maintain
  • New utilities: field-sizing, starting-style, not-* variant, inert, masking utilities
  • CSS variables for everything: every design token is a CSS custom property
  • Composable variants: group-hover/sidebar, peer-focus/input named groups and peers

Installation

# With Vite (recommended)
npm install tailwindcss @tailwindcss/vite

# With PostCSS (for non-Vite projects)
npm install tailwindcss @tailwindcss/postcss

Vite config:

// vite.config.js
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
    plugins: [tailwindcss()],
})

Then in your main CSS file, just one line:

@import "tailwindcss";

That's it. No @tailwind base, no @tailwind components, no @tailwind utilities. No tailwind.config.js.

CSS-First Configuration with @theme

In v3, you customized Tailwind in JavaScript:

// tailwind.config.js (v3)
module.exports = {
    theme: {
        extend: {
            colors: {
                brand: {
                    500: '#2563eb',
                    600: '#1d4ed8',
                },
            },
            fontFamily: {
                sans: ['Inter', 'sans-serif'],
            },
        },
    },
}

In v4, you do this in CSS:

@import "tailwindcss";

@theme {
    --color-brand-500: #2563eb;
    --color-brand-600: #1d4ed8;
    --font-sans: 'Inter', sans-serif;
    --spacing-18: 4.5rem;
    --radius-xl: 1rem;
}

These @theme variables become CSS custom properties at :root, so you can also use them directly in non-Tailwind CSS:

.hero {
    background-color: var(--color-brand-500);
    border-radius: var(--radius-xl);
}

And the Tailwind utility classes are generated automatically: bg-brand-500, text-brand-600, rounded-xl.

Automatic Content Detection

In v3, you had to specify where Tailwind should look for class names:

// tailwind.config.js (v3)
content: ['./src/**/*.{html,js,ts,jsx,tsx}'],

In v4, Tailwind automatically scans your project using the same logic as Vite — it reads your project's file tree and scans all files that could contain class names. You almost never need to override this. If you have edge cases (e.g., class names stored in a database or generated at runtime), use @source:

@import "tailwindcss";
@source "./src/templates/**/*.html";   /* explicit additional source */
@source inline("bg-red-500 text-lg");  /* inline safelisting */

New Utilities Worth Knowing

field-sizing — Auto-Resizing Textareas

<textarea class="field-sizing-content w-full"></textarea>

The field-sizing: content CSS property makes textareas grow with their content automatically, with no JavaScript required. Tailwind v4 exposes this as field-sizing-content and field-sizing-fixed.

not-* Variant

<!-- Style all children that are NOT the last -->
<li class="not-last:border-b border-gray-200">Item</li>

<!-- Style when NOT hovered -->
<button class="not-hover:opacity-70">Click me</button>

The not-* variant maps to the :not() CSS pseudo-class. You can negate any selector variant: not-focus:ring-0, not-disabled:cursor-pointer, etc.

starting-style — Entry Animations Without JavaScript

<dialog class="
    opacity-100 transition-all duration-300
    starting:opacity-0 starting:translate-y-2
">...</dialog>

The @starting-style CSS rule (now exposed as the starting: variant) defines the styles from which an element transitions when it first appears. This enables enter animations for dialogs, popovers, and elements entering the DOM without JavaScript.

Named Groups and Peers

<nav class="group/sidebar">
    <a href="#" class="group-hover/sidebar:text-blue-500">Home</a>
</nav>

<form>
    <input type="email" class="peer/email" />
    <p class="hidden peer-invalid/email:block text-red-500">
        Please enter a valid email.
    </p>
</form>

Named groups (group/name) and named peers (peer/name) let you scope group/peer interactions when you have nested or sibling groups — eliminating the need for workarounds that were common in v3.

Migrating from Tailwind v3

Step 1: Update packages

npm uninstall tailwindcss postcss autoprefixer
npm install tailwindcss @tailwindcss/vite   # or @tailwindcss/postcss for non-Vite

# Remove old config files
rm tailwind.config.js postcss.config.js

Step 2: Update your CSS entry file

/* Before (v3) */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* After (v4) */
@import "tailwindcss";

Step 3: Move theme customizations to @theme

/* Before: tailwind.config.js theme.extend.colors */
/* After: in your CSS file */
@theme {
    --color-primary: #2563eb;
    --color-primary-dark: #1d4ed8;
}

Step 4: Update renamed utilities

Some utilities changed names in v4. The official upgrade tool handles most automatically:

npx @tailwindcss/upgrade

Notable renames: shadow-smshadow-xs, ring (default 3px) → ring-3, blurblur-sm, inset-* now also covers top/right/bottom/left independently.

Step 5: Handle custom plugins

The plugin API is unchanged but plugins are now registered in CSS:

@import "tailwindcss";
@plugin "@tailwindcss/forms";
@plugin "@tailwindcss/typography";

Performance Comparison

On a large Next.js project (500+ components, extensive custom theme):

  • Full build v3: ~2.3 seconds → v4: ~0.28 seconds (8× faster)
  • HMR incremental build v3: ~140ms → v4: ~12ms
  • Cold install with Vite: noticeably snappier feedback loop

The difference is most pronounced on larger codebases. Small projects see a 3–4× improvement; large monorepos with shared design systems see the biggest gains.

Summary

Tailwind CSS v4 is a major quality-of-life upgrade: faster builds, simpler setup, and CSS-native customization that integrates naturally with design systems and CSS custom properties. The migration is largely automated by the upgrade tool. For new projects, there's no reason to start with v3. For existing v3 projects, the migration is low-risk and the build speed improvement pays for itself quickly.

Frequently Asked Questions

Do I still need PostCSS with Tailwind v4?

Not with Vite. The @tailwindcss/vite plugin handles everything. For non-Vite setups (Next.js with webpack, Parcel, plain PostCSS pipelines), use @tailwindcss/postcss and a minimal postcss.config.mjs with just the Tailwind plugin. You no longer need autoprefixer — Tailwind v4 handles vendor prefixes internally.

Is tailwind.config.js still supported in v4?

No. The JavaScript config file is removed in v4. Configuration moves to the CSS @theme directive. If you have a complex config with custom plugins written as JavaScript functions, those plugins can still be defined in JS files and referenced via @plugin "./my-plugin.js". But theme tokens, content patterns, and most customizations move to CSS.

Does the @tailwindcss/upgrade tool handle the entire migration?

It handles the majority: updating class names that changed, converting @tailwind directives to @import, and migrating simple theme values to @theme. Complex plugin logic and highly customized configs may need manual review. Run the tool, check the diff, test your UI, and address anything the tool flagged as needing manual attention. Most v3 projects migrate in under an hour.

Can I use Tailwind v4 with React, Vue, and Svelte?

Yes. Tailwind v4 works with any frontend framework. Use @tailwindcss/vite for Vite-based projects (Vite + React, Vite + Vue, SvelteKit). For Next.js, use @tailwindcss/postcss — Next.js 15+ has first-class Tailwind v4 support. For Nuxt, Laravel Mix, and others, use the PostCSS plugin. The JSX/template class detection is automatic regardless of framework.