EmpireUI
Get Pro
← Blog8 min read#tailwind-css#tailwind-v4#css

Tailwind CSS v4: Everything That Changed and How to Migrate

Tailwind CSS v4 rewrites the engine, drops the config file, and ships native CSS cascade layers. Here's what actually changed and how to migrate without breaking your project.

Code editor showing CSS and Tailwind utility classes on a dark screen

What Actually Changed in Tailwind CSS v4

Honestly, Tailwind v4 is not an incremental update. It's a ground-up rewrite of the engine. The PostCSS plugin is gone as the primary entry point. The JavaScript config file is optional. The entire framework now ships as a single CSS import that does its own source scanning at build time.

The biggest architectural shift is that Tailwind v4 leans on native CSS features — cascade layers, registered custom properties via @property, and color-mix() — instead of generating thousands of static utility classes up front. Your output CSS is smaller because it only includes what you actually use, and the engine is faster because it's written in Rust (via the new Oxide engine).

Version 4.0.0 dropped in early 2025, and by v4.0.2 the migration story had solidified. If you're still on v3.4.x, the concepts are familiar but the file structure looks completely different. That's the part that trips people up.

The New CSS-First Configuration Model

In v3, you configured everything in tailwind.config.js — theme extensions, plugins, content globs, the works. In v4, that file is optional. Most configuration moves into your main CSS file using @theme blocks and @source directives.

Here's what a v4 project entry CSS looks like compared to what you might be used to:

/* v4 — your main app.css */
@import "tailwindcss";

@theme {
  --font-sans: 'Inter', sans-serif;
  --color-brand: oklch(62% 0.2 250);
  --spacing-18: 4.5rem;
  --radius-card: 12px;
}

@source "../components/**/*.tsx";
@source "../pages/**/*.tsx";

That @theme block replaces theme.extend in your old config. Every token you define there becomes a CSS custom property AND generates the corresponding utility classes automatically. So --color-brand gives you text-brand, bg-brand, border-brand, and so on — no plugin needed. It's genuinely clever once you stop expecting the old mental model.

Breaking Changes You'll Hit First

The rename list is the first thing that will break your build. Tailwind v4 renamed a handful of utilities. shadow-sm is now shadow-xs, the old shadow is now shadow-sm, and ring defaults changed. bg-opacity-* is gone entirely — you use bg-black/50 syntax instead, which was technically available in v3 but is now the only way.

The content configuration is also gone. You no longer list content globs in a config file. Instead you use @source directives in your CSS, or you let Tailwind auto-detect based on your framework (Vite, Next.js, etc). If you had a monorepo with weird paths, you'll need to be explicit with @source.

Prefix support changed too. In v3 you set prefix: 'tw-' in the config. In v4 it's @import "tailwindcss" prefix(tw) in your CSS. Small thing, but it'll break your search-and-replace if you forget it. Also worth noting: the dark: variant now defaults to the prefers-color-scheme media query, not the class strategy. If your app toggles dark mode with a .dark class on <html>, you need to opt back in via @variant dark (&:where(.dark, .dark *)); — check out implementing a theme toggle in React to see the full pattern.

OKLCH Colors Are Now First-Class in v4

Tailwind v4 ships its entire default color palette in OKLCH. Every --color-blue-500 and friends are now defined in oklch() space instead of hex. That's not just aesthetic — it means color interpolation in gradients looks perceptually uniform, and you can manipulate colors with color-mix() natively in CSS.

The practical impact: oklch(62% 0.2 250) for a blue brand color will look the same saturation-wise across the full lightness spectrum. The old hex-based palette had visible weirdness in gradients at the mid-range. If you're doing glassmorphism effects or complex backgrounds, this matters more than you think — we cover the specifics in using OKLCH colors with Tailwind.

Custom theme colors in v4 should also be in OKLCH if you want full interoperability with color-mix(). You can still use hex, but then you lose the native mixing behavior. The transition is worth it for anything more than a landing page.

Migrating from v3: Step-by-Step

First, update your dependencies. Remove tailwindcss, autoprefixer, and postcss as separate packages if you're using Vite — in v4, the Vite plugin (@tailwindcss/vite) handles everything. For Next.js you use @tailwindcss/postcss instead of the old tailwindcss PostCSS plugin.

# Remove old setup
npm remove tailwindcss autoprefixer postcss

# Install v4 (Vite project)
npm install tailwindcss@^4 @tailwindcss/vite@^4

# Or for Next.js / PostCSS
npm install tailwindcss@^4 @tailwindcss/postcss@^4

Then run the official codemod to handle the bulk of the rename changes: npx @tailwindcss/upgrade. It'll rewrite your CSS entry point, update utility names it recognizes, and flag things it can't auto-fix. It doesn't handle everything — custom plugins and complex theme() function calls in your CSS need manual work — but it gets you 80% there. After that, do a full build and grep the output for any utility that's generating nothing. Those are usually the remaining renames.

Container Queries Without the Plugin

In v3, container queries required @tailwindcss/container-queries as a separate plugin. In v4, they're built in. You get @container, @sm:, @md:, @lg: variants out of the box with zero config. This is one of the most underrated changes in the release.

The syntax is identical to what the plugin offered: set @container on a parent, then use @md:flex on children to respond to the container's width rather than the viewport. For component-driven design — cards, sidebars, reusable UI blocks — this eliminates a whole category of awkward breakpoint hacks. We've written more about this pattern in Tailwind container queries for component layouts.

One thing that's new in v4 that wasn't in the plugin: named containers. You can do @container/sidebar and then use @sidebar-md:hidden to target that specific named container. Useful when you have nested containment contexts and need to be explicit about which ancestor you're querying.

Custom Utilities and Variants in v4

Plugins in v3 used a JavaScript API — addUtilities(), addVariant(), matchUtilities(). In v4, you write CSS directly with @utility and @variant. No JavaScript required for most use cases.

/* Define a custom utility */
@utility glass {
  background: rgba(255, 255, 255, 0.15);
  backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, 0.2);
}

/* Define a custom variant */
@variant hocus {
  &:hover,
  &:focus-visible {
    @slot;
  }
}

Now you can write hocus:text-brand or glass directly in your HTML. The @utility block supports arbitrary values too — @utility gap-* with gap: --value(--spacing-*); will generate gap-8 as 8px gap using your theme spacing scale. This replaces matchUtilities() from v3 plugins with something you can write in pure CSS. For glassmorphism effects in particular, the @utility approach is much cleaner than what you'd do in v3 — see advanced glassmorphism techniques with Tailwind for real-world examples.

Performance: Is v4 Actually Faster?

Yes. Measurably. The Oxide engine written in Rust handles scanning and class detection significantly faster than the old Node.js-based JIT engine. On large projects with hundreds of components, cold build times drop by 30-60%. Incremental rebuilds — the ones that happen on every file save — are even faster because the engine is smarter about what changed.

What's the catch? The first build might feel similar if your framework adds overhead (Next.js cold starts, TypeScript compilation). The gains are most visible in Vite projects where Tailwind's scan is the bottleneck. Also, the CSS output itself tends to be slightly larger in v4 because of the registered @property declarations for animated values, but gzip compression handles that fine in practice.

Are there still edge cases where v4 generates a class you expect and just... doesn't? Yes. The scanning heuristics are different, and dynamically constructed class names (string concatenation, template literals without static analysis) still don't work — that's not new, but it catches people on the upgrade. Keep your @source directives pointed at the right files and you'll be fine.

FAQ

Do I still need a tailwind.config.js file in v4?

No. The config file is optional in v4. All theme configuration moves into your CSS file using @theme blocks. You only need a JS config if you're using v3-compatible plugins that haven't migrated to the new CSS-based API yet.

Why did my dark mode stop working after upgrading to v4?

Tailwind v4 defaults dark mode to the prefers-color-scheme media query. If your app uses a .dark class on the <html> element, add this to your CSS: @variant dark (&:where(.dark, .dark *)); to restore class-based dark mode.

The `@tailwindcss/upgrade` codemod ran but my build still has errors. What did it miss?

The codemod handles most utility renames and config-to-CSS migrations, but it skips custom plugins using the JavaScript API, theme() function calls in arbitrary CSS, and any dynamically constructed class strings. Check those manually. Also verify your @source directives cover all component files — missing sources means missing classes.

Can I use Tailwind v4 with Next.js App Router?

Yes. Install @tailwindcss/postcss@^4 and update your postcss.config.js to use @tailwindcss/postcss instead of tailwindcss. The @import 'tailwindcss' line in your global CSS replaces the old @tailwind base/components/utilities directives.

What happened to `bg-opacity-*` and `text-opacity-*` utilities?

They're removed in v4. Use the slash opacity syntax instead: bg-black/50, text-white/80. This syntax was available in v3 too, so your components may already use it — but any remaining bg-opacity-* classes need to be converted manually since the codemod doesn't always catch them inside complex JSX.

How do I define a custom color token that generates full utility classes in v4?

Add it to your @theme block: --color-brand: oklch(62% 0.2 250);. Tailwind v4 automatically generates text-brand, bg-brand, border-brand, ring-brand, fill-brand, and all other color utilities from any --color-* token you define. No plugin or extend block needed.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Tailwind CSS Mastery: Every Utility, Plugin, and Pattern in One GuideBuilding UI Components with Tailwind: Patterns That ScaleCSS Scroll-Driven Animations: No JavaScript, Full Browser SupportNative CSS Nesting: Full Guide with Real Component Examples