WebPiki
tutorial

Tailwind CSS v4 Migration: What Changed from v3

Key changes in Tailwind CSS v4 and how to migrate from v3. CSS-based config, new utilities, and the Oxide engine.

New colors painting a style transformation

Tailwind CSS v4 looks similar to v3 on the surface, but the internals changed substantially. The most visible shift: tailwind.config.js is gone. Configuration now lives directly in your CSS file. It feels odd at first, but once you get used to it, it's actually more intuitive.

If you've been maintaining a v3 project for a while, the migration might seem daunting — but honestly the upgrade tool handles most of the grunt work. This article walks through what actually changed, what breaks, and what to watch for when you pull the trigger.

The Biggest Change — Config Moved to CSS

In v3, you defined colors, fonts, breakpoints, and other design tokens as a JavaScript object in tailwind.config.js. In v4, you define them inside your CSS file using the @theme directive.

/* v3: tailwind.config.js */
/* module.exports = {
  theme: {
    extend: {
      colors: {
        brand: '#3b82f6',
      },
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
      },
    },
  },
} */

/* v4: app.css */
@import "tailwindcss";

@theme {
  --color-brand: #3b82f6;
  --font-sans: 'Inter', sans-serif;
}

You're defining CSS custom properties directly. No JavaScript config file means a simpler build chain, and you can see all your design tokens by just looking at the CSS.

One subtle benefit: since @theme values are CSS custom properties, you can reference them anywhere in your stylesheet with var(). In v3, design tokens only existed inside Tailwind's build step — if you needed a brand color in a custom CSS block, you'd either hardcode it or use Tailwind's theme() function. Now it's just standard CSS.

@theme {
  --color-brand: #3b82f6;
}

/* Use anywhere — no theme() needed */
.custom-element {
  border-left: 3px solid var(--color-brand);
}

What About extend?

In v3, theme.extend was how you added tokens without overriding defaults. v4 handles this differently: @theme always extends by default. If you want to remove all default values for a namespace and define only your own, reset with initial:

@theme {
  --color-*: initial;  /* removes all default colors */
  --color-brand: #3b82f6;
  --color-gray: #6b7280;
}

This catches some people off guard when they want a minimal palette. Without the initial reset, the full default color set stays available.

No More content Configuration

The content array was one of the most common sources of frustration in v3. You had to specify which files use Tailwind classes, and forgetting a path meant styles silently didn't apply.

// v3: everyone has gotten bitten by this at least once
module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
}

v4 uses automatic content detection by default. It scans your project directory and finds classes on its own. No content config needed. For some people, this alone is reason enough to migrate.

If you need to exclude or include specific paths, use the @source directive:

@import "tailwindcss";
@source "../node_modules/some-ui-lib";

The auto-detection respects .gitignore rules, so files you don't track won't be scanned. If you're pulling classes from a dependency (like a UI library in node_modules), @source is how you explicitly tell Tailwind to look there.

New Engine — Oxide

v4 runs on a new Rust-based engine called Oxide. What you'll notice:

Build speed — Significantly faster than v3, especially on large projects. Instead of running as a PostCSS plugin, it operates as a standalone CSS processing engine.

Simpler installation — No need to install postcss and autoprefixer separately. One plugin handles everything: @tailwindcss/postcss or @tailwindcss/vite.

# v3
npm install tailwindcss postcss autoprefixer

# v4
npm install tailwindcss @tailwindcss/vite  # Vite projects

Changed Utilities

Color Opacity

<!-- v3 -->
<div class="bg-blue-500/50">

<!-- v4 — same syntax still works, plus CSS variable control -->
<div class="bg-blue-500/50">
<div class="bg-[oklch(0.5_0.2_250/50%)]">

v4 uses the oklch color space internally. You can use oklch values directly, which is useful because oklch is perceptually uniform — maintaining consistent lightness across a color palette becomes much easier.

New Utilities

<!-- Container queries -->
<div class="@container">
  <div class="@lg:flex @md:grid">...</div>
</div>

<!-- 3D transforms -->
<div class="rotate-x-45 perspective-distant">

<!-- Gradient improvements -->
<div class="bg-linear-to-r from-blue-500 to-purple-500">
<!-- bg-gradient-to-r → bg-linear-to-r -->

Container queries are now built in. In v3, you needed the @tailwindcss/container-queries plugin. v4 includes them natively. Components can adapt their layout based on their parent's size rather than the viewport — a big deal for reusable components.

Custom Utilities with @utility

v3 had @layer utilities for custom classes, but the behavior around specificity was sometimes confusing. v4 introduces @utility as a first-class way to define custom utilities that integrate cleanly with variants:

@utility scrollbar-hidden {
  scrollbar-width: none;
  &::-webkit-scrollbar {
    display: none;
  }
}

Now hover:scrollbar-hidden, md:scrollbar-hidden, or any other variant works out of the box. In v3, getting custom utilities to play nicely with all variants required more boilerplate.

Renamed Utilities

Some utilities got more consistent names:

v3v4
bg-gradient-to-rbg-linear-to-r
bg-opacity-50bg-black/50 (already worked in v3)
shadow-smshadow-xs (new size step added)

Most v3 syntax still works, and the automated migration tool handles the renamed ones.

Migration in Practice

1. The Automatic Migration Tool

An official tool is available:

npx @tailwindcss/upgrade

What it does:

  • Converts tailwind.config.js settings to CSS @theme blocks
  • Renames changed utility classes automatically
  • Updates import structure
  • Cleans up PostCSS config

It's not perfect, but it handles the bulk of the conversion. Fix the rest manually.

2. Watch Out For

Plugin compatibility — v3 plugins won't necessarily work in v4. Official plugins like @tailwindcss/typography and @tailwindcss/forms have been updated, but community plugins may need checking.

PostCSS config changes — v4 uses a dedicated plugin (@tailwindcss/postcss or @tailwindcss/vite) instead of the PostCSS plugin approach. Your postcss.config.js needs updating.

Complex JS-based customizations — If you used functions in tailwind.config.js to dynamically generate values, those won't translate 1:1 to CSS @theme. You'll need to combine CSS custom properties with the @utility directive to achieve the same result.

Dark mode changes — v4 defaults to the @media (prefers-color-scheme: dark) strategy. If you were using class strategy in v3 (toggling via a .dark class on <html>), add this to your CSS:

@custom-variant dark (&:where(.dark, .dark *));

This one is easy to miss and causes the "my dark mode stopped working" issue that shows up in almost every migration.

Removed deprecated utilitiesbg-opacity-*, text-opacity-*, and similar opacity utilities are gone. Use the slash syntax instead: bg-black/50. The upgrade tool catches most of these, but check manually if you have utilities generated dynamically (e.g., via template literals in JS).

  1. Start from a clean git commit
  2. Run npx @tailwindcss/upgrade
  3. Build and check for errors
  4. Visually inspect pages, fixing broken styles manually
  5. Verify plugin compatibility

For large projects, check page by page rather than trying to verify everything at once.

Framework-Specific Notes

Next.js

If you're using Next.js with the App Router, swap the PostCSS plugin:

// postcss.config.mjs
export default {
  plugins: {
    '@tailwindcss/postcss': {},
  },
};

Remove autoprefixer from your config — the new plugin handles vendor prefixes internally.

Vite

Vite users get the cleanest setup. Replace PostCSS entirely with the Vite plugin:

// vite.config.ts
import tailwindcss from '@tailwindcss/vite';

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

No postcss.config.js needed at all. This is the fastest path because the Vite plugin hooks directly into the build pipeline.

Should You Migrate?

New projects — start with v4. Simpler config, faster builds, more features.

Existing projects — no rush. v3 will be maintained for a while. But if you want container queries, 3D transforms, or faster builds, there's plenty of reason to migrate. The automated tool makes it a smaller effort than it might seem.

The biggest practical win? Not having to maintain tailwind.config.js alongside your CSS. Design tokens, custom utilities, source paths — it all lives in one place now. For teams, that means fewer "where is this configured?" questions and one less file to review in PRs.

#Tailwind CSS#CSS#Frontend#Migration#Web Development

Related Posts