Skip to main content

Migrating to Tailwind CSS v4: Key Changes and Breaking Layouts

June 2, 2026

</>

Tailwind CSS v4 is not an incremental update—it is a complete rewrite of how the framework is configured and compiled. The JavaScript configuration file you have relied on for years (tailwind.config.js) is gone. In its place is a CSS-first configuration model powered by a new Rust-compiled engine called Oxide, which builds stylesheets up to 10x faster and natively supports modern CSS features like container queries, cascade layers, and CSS custom properties out of the box.

This post details the architectural shift, provides a practical step-by-step migration guide, explains the most common layout regressions, and shows how to verify your upgrade worked correctly.


Architectural Shift: From JS Config to CSS-First

In Tailwind v3, all customization lived in a Node-based tailwind.config.js file. Themes, colors, font families, and custom plugins were defined as JavaScript objects and processed at build time.

In v4, your main CSS stylesheet becomes the single source of configuration. Theme values, custom properties, and utility extensions are declared using the new @theme CSS directive.

Before: tailwind.config.js (v3)

// tailwind.config.js (v3 — now deleted)
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: '#b91c1c',
        'brand-light': '#ef4444',
      },
      fontFamily: {
        sans: ['Ubuntu', 'sans-serif'],
      },
    },
  },
  plugins: [],
}

After: global.css (v4)

/* app/global.css (v4) */
@import "tailwindcss";

@theme {
  --color-brand: #b91c1c;
  --color-brand-light: #ef4444;
  --font-sans: "Ubuntu", "sans-serif";
}

Every variable declared inside @theme automatically generates the corresponding Tailwind utility class (bg-brand, text-brand, text-brand-light) and exposes it as a native CSS variable (var(--color-brand)) for use anywhere in your stylesheets.


Step-by-Step Migration Guide

Step 1: Run the Automated Upgrade Tool

The Tailwind team ships an official upgrade codemod that handles the most common file transformations automatically:

npx @tailwindcss/upgrade@next

This tool migrates your tailwind.config.js to a @theme block, updates your PostCSS config, and flags classes that need manual review.

Step 2: Install the Updated Packages

After the codemod runs, install the required v4 package set:

# Using pnpm
pnpm add tailwindcss@4 @tailwindcss/postcss@4 postcss@8

Step 3: Update Your PostCSS Config

Replace the old tailwindcss PostCSS plugin with the new dedicated wrapper:

// postcss.config.cjs
module.exports = {
  plugins: {
    '@tailwindcss/postcss': {},
  }
}

Step 4: Rewrite Your Global CSS Entrypoint

Replace the three v3 directives with a single @import statement and move theme tokens into @theme:

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

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

@theme {
  --font-sans: var(--font-ubuntu), ui-sans-serif, system-ui;
  --font-mono: var(--font-jetbrains), ui-monospace, monospace;
}

Step 5: Delete tailwind.config.js

The Oxide engine automatically detects template files by scanning your project without manual content path declarations. The config file is no longer needed.


Troubleshooting Common Layout Regressions

1. Dynamic Class Concatenation Breaks

Tailwind's static compiler extracts utility classes by scanning file tokens at build time. V4 is stricter—string concatenations using template literals are not parsed.

// ❌ Breaks in v4 (compiler cannot statically resolve this)
const textColor = `text-${color}-500`;

// ✅ Safe: use a complete static lookup map
const colorMap: Record<string, string> = {
  red: 'text-red-500',
  blue: 'text-blue-500',
  green: 'text-green-500',
};
const textColor = colorMap[color];

2. Font Family Fallback Behavior Changed

In v3, setting a custom fontFamily in the theme merged with the default browser fallback stack. In v4, setting --font-sans in @theme replaces the fallback list entirely.

Fix: Always append standard system fallbacks to your custom fonts:

@theme {
  /* Always include fallbacks after your custom font */
  --font-sans: "Ubuntu", ui-sans-serif, system-ui, sans-serif;
}

3. Removed Utility Classes

Several v3 utilities were removed or renamed in v4:

  • decoration-slicebox-decoration-slice
  • decoration-clonebox-decoration-clone
  • overflow-ellipsistext-ellipsis
  • flex-grow-*grow-*

The upgrade codemod handles most of these automatically, but run a search across your templates for any missed instances after migration.

4. Z-Index and Stacking Context

V4 changes how custom utilities are compiled relative to browser defaults. If you observe unexpected stacking or overlay issues, explicitly declare position: relative or position: absolute on elements alongside z-* classes.


Verifying Your Migration

After migrating, run a visual regression pass to catch missed regressions:

# Build and inspect the output CSS bundle for unexpected changes
pnpm build

# Run Playwright visual snapshot tests
npx playwright test --project=chromium tests/visual.spec.ts

# Check bundle size hasn't regressed
npx @next/bundle-analyzer

The new Oxide engine typically reduces your final CSS bundle size significantly because unused utilities are eliminated more aggressively. A 30–60% reduction in CSS output is common on large codebases.


Conclusion

Tailwind CSS v4 requires upfront migration effort, but the payoff is substantial: faster build times, a cleaner configuration model, native CSS variable integration, and smaller output bundles. The key to a smooth migration is running the official upgrade codemod first, then manually auditing dynamic class names and font fallback stacks. For most Next.js and Vite projects, the full migration takes a focused afternoon—and the resulting developer experience is significantly cleaner.

Recommended Posts