DEV Community

HK Lee
HK Lee

Posted on • Originally published at pockit.tools

Tailwind CSS v4 Migration Guide: Everything That Changed and How to Upgrade (2026)

Tailwind CSS v4 isn't just another minor version bump—it's a complete rewrite. The team rebuilt the engine from scratch using Rust and Oxide, changed how configuration works, and dropped the PostCSS plugin entirely. If you've been putting off the upgrade, this guide will walk you through everything that changed and exactly how to migrate.

After upgrading three production apps, I can tell you: the pain is worth it. Build times dropped 3-10x, CSS output is smaller, and the new CSS-first config is actually nicer to work with once you get used to it.

Let's break down what changed and how to handle each part of the migration.

What Actually Changed in Tailwind v4?

Before diving into the migration steps, you need to understand the architectural shifts:

1. New Engine Written in Rust (Oxide)

Tailwind v3 used a JavaScript-based engine. v4 uses Oxide, a new high-performance engine written in Rust. This isn't just a rewrite—it's fundamentally faster:

Metric v3 v4 Improvement
Initial build ~800ms ~100ms 8x faster
Incremental rebuild ~200ms ~5ms 40x faster
CSS output size 24KB 18KB 25% smaller

The Rust engine also means better parallelization and lower memory usage. Hot module replacement (HMR) during development feels instant now.

2. CSS-First Configuration

This is the biggest mental shift. In v3, you configured everything in tailwind.config.js:

// v3: tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: '#3b82f6',
        secondary: '#10b981',
      },
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
      },
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

In v4, configuration moves into your CSS file using @theme:

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

@theme {
  --color-primary: #3b82f6;
  --color-secondary: #10b981;
  --font-sans: "Inter", sans-serif;
}
Enter fullscreen mode Exit fullscreen mode

Why this matters:

  • Configuration lives with your styles
  • Better IDE autocomplete for custom values
  • CSS custom properties are native—inspect them in DevTools
  • No separate JS config file to maintain

3. No More PostCSS Plugin

In v3, Tailwind was a PostCSS plugin:

// v3: postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}
Enter fullscreen mode Exit fullscreen mode

In v4, Tailwind is a standalone CLI or Vite plugin. PostCSS is still supported for compatibility, but it's not the default:

// v4: vite.config.ts (recommended)
import tailwindcss from '@tailwindcss/vite'

export default {
  plugins: [tailwindcss()],
}
Enter fullscreen mode Exit fullscreen mode
# v4: CLI alternative
npx @tailwindcss/cli -i input.css -o output.css --watch
Enter fullscreen mode Exit fullscreen mode

4. Automatic Content Detection

Remember configuring content paths in v3?

// v3: tailwind.config.js
module.exports = {
  content: [
    './src/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
}
Enter fullscreen mode Exit fullscreen mode

In v4, Tailwind automatically detects which files use Tailwind classes by analyzing your project. No configuration needed for most projects. If you need to customize:

/* v4: Override auto-detection */
@import "tailwindcss";

@source "../node_modules/some-ui-library";
Enter fullscreen mode Exit fullscreen mode

5. Native CSS Cascade Layers

v4 uses CSS @layer natively (not Tailwind's custom implementation):

/* v4 generates real CSS layers */
@layer theme, base, components, utilities;

@layer utilities {
  .text-primary { color: var(--color-primary); }
}
Enter fullscreen mode Exit fullscreen mode

This gives you proper cascade control and better integration with other CSS.

Step-by-Step Migration Guide

Now let's actually migrate. I'll show you the exact steps for a typical React/Next.js project.

Step 1: Update Dependencies

Remove old Tailwind packages and install v4:

# Remove v3 packages
npm uninstall tailwindcss postcss autoprefixer

# Install v4
npm install tailwindcss@latest

# If using Vite, add the plugin
npm install @tailwindcss/vite

# If using Next.js, add the PostCSS compatibility package
npm install @tailwindcss/postcss
Enter fullscreen mode Exit fullscreen mode

Step 2: Update Your Build Configuration

For Vite projects:

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [
    react(),
    tailwindcss(),
  ],
})
Enter fullscreen mode Exit fullscreen mode

For Next.js projects:

Next.js 15+ has built-in Tailwind v4 support. For older versions:

// postcss.config.js (Next.js compatibility)
module.exports = {
  plugins: {
    '@tailwindcss/postcss': {},
  },
}
Enter fullscreen mode Exit fullscreen mode

For standalone CLI:

# Add to package.json scripts
"scripts": {
  "css:build": "npx @tailwindcss/cli -i src/input.css -o dist/output.css",
  "css:watch": "npx @tailwindcss/cli -i src/input.css -o dist/output.css --watch"
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Convert Your CSS Entry Point

This is where most of the work happens. Your main CSS file needs to be rewritten.

Before (v3):

/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Custom styles below */
.btn-primary {
  @apply bg-blue-500 text-white px-4 py-2 rounded;
}
Enter fullscreen mode Exit fullscreen mode

After (v4):

/* globals.css */
@import "tailwindcss";

/* Custom styles - now use @layer properly */
@layer components {
  .btn-primary {
    @apply bg-blue-500 text-white px-4 py-2 rounded;
  }
}
Enter fullscreen mode Exit fullscreen mode

The key changes:

  • @tailwind base/components/utilities@import "tailwindcss"
  • Custom component classes should be wrapped in @layer components
  • Utility overrides go in @layer utilities

Step 4: Migrate tailwind.config.js to @theme

This is the trickiest part. You need to convert JavaScript configuration to CSS custom properties.

Before (v3 config):

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#eff6ff',
          100: '#dbeafe',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
        },
        accent: '#f59e0b',
      },
      spacing: {
        '18': '4.5rem',
        '88': '22rem',
      },
      fontSize: {
        'xxs': '0.625rem',
      },
      borderRadius: {
        '4xl': '2rem',
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
        mono: ['JetBrains Mono', 'monospace'],
      },
      screens: {
        'xs': '475px',
        '3xl': '1920px',
      },
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

After (v4 @theme):

/* globals.css */
@import "tailwindcss";

@theme {
  /* Colors use --color-* prefix */
  --color-primary-50: #eff6ff;
  --color-primary-100: #dbeafe;
  --color-primary-500: #3b82f6;
  --color-primary-600: #2563eb;
  --color-primary-700: #1d4ed8;
  --color-accent: #f59e0b;

  /* Spacing uses --spacing-* prefix */
  --spacing-18: 4.5rem;
  --spacing-88: 22rem;

  /* Font size uses --text-* prefix */
  --text-xxs: 0.625rem;

  /* Border radius uses --radius-* prefix */
  --radius-4xl: 2rem;

  /* Font family uses --font-* prefix */
  --font-sans: "Inter", system-ui, sans-serif;
  --font-mono: "JetBrains Mono", monospace;

  /* Breakpoints use --breakpoint-* prefix */
  --breakpoint-xs: 475px;
  --breakpoint-3xl: 1920px;
}
Enter fullscreen mode Exit fullscreen mode

Variable Naming Cheat Sheet

v3 Config Key v4 CSS Variable Prefix Example
colors --color-* --color-primary-500
spacing --spacing-* --spacing-18
fontSize --text-* --text-xxs
fontFamily --font-* --font-sans
fontWeight --font-weight-* --font-weight-medium
lineHeight --leading-* --leading-relaxed
letterSpacing --tracking-* --tracking-wide
borderRadius --radius-* --radius-4xl
borderWidth --border-* --border-3
boxShadow --shadow-* --shadow-soft
screens --breakpoint-* --breakpoint-xs
zIndex --z-* --z-dropdown
opacity --opacity-* --opacity-15

Step 5: Handle Plugins

Most v3 plugins need updates or have been absorbed into core.

Typography Plugin:

npm install @tailwindcss/typography@latest
Enter fullscreen mode Exit fullscreen mode
/* v4: Import the plugin in CSS */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
Enter fullscreen mode Exit fullscreen mode

Forms Plugin:

npm install @tailwindcss/forms@latest
Enter fullscreen mode Exit fullscreen mode
@import "tailwindcss";
@plugin "@tailwindcss/forms";
Enter fullscreen mode Exit fullscreen mode

Container Queries Plugin:

Now built into core! No plugin needed:

<!-- Works natively in v4 -->
<div class="@container">
  <div class="@md:grid-cols-2">...</div>
</div>
Enter fullscreen mode Exit fullscreen mode

Step 6: Update Deprecated Utilities

Some utilities were renamed or changed:

v3 Class v4 Class Notes
bg-opacity-50 bg-blue-500/50 Opacity modifier syntax
text-opacity-75 text-gray-900/75 Same pattern
decoration-slice box-decoration-slice Renamed
decoration-clone box-decoration-clone Renamed
overflow-ellipsis text-ellipsis Renamed
flex-grow-0 grow-0 Simplified
flex-shrink shrink Simplified

Opacity modifiers are now the standard:

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

<!-- v4 -->
<div class="bg-blue-500/50">...</div>
Enter fullscreen mode Exit fullscreen mode

Step 7: Update Dark Mode

Dark mode still works the same way, but configuration moved:

/* v4: Enable class-based dark mode */
@import "tailwindcss";

@variant dark (&:where(.dark, .dark *));
Enter fullscreen mode Exit fullscreen mode

Or use the default (prefers-color-scheme):

/* This is the default - no config needed */
@import "tailwindcss";
Enter fullscreen mode Exit fullscreen mode

Common Migration Problems and Solutions

After migrating several projects, here are the issues you'll likely hit:

Problem 1: "Unknown at-rule @tailwind"

Your editor/linter is still expecting v3 syntax.

Solution: Update your CSS file to use @import "tailwindcss" instead of the old directives.

Problem 2: Custom Colors Not Working

Symptom: bg-primary-500 doesn't work after migration.

Solution: Make sure your color variables use the exact naming convention:

/* Wrong */
--primary-500: #3b82f6;

/* Right */
--color-primary-500: #3b82f6;
Enter fullscreen mode Exit fullscreen mode

Problem 3: PostCSS Errors in Next.js

Symptom: Build fails with PostCSS-related errors.

Solution: Make sure you're using the compatibility package:

npm install @tailwindcss/postcss
Enter fullscreen mode Exit fullscreen mode
// postcss.config.js
module.exports = {
  plugins: {
    '@tailwindcss/postcss': {},
  },
}
Enter fullscreen mode Exit fullscreen mode

Problem 4: Plugins Not Loading

Symptom: Typography or forms plugin classes don't work.

Solution: Plugins now load via CSS, not config:

@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";
Enter fullscreen mode Exit fullscreen mode

Problem 5: Content Detection Missing Files

Symptom: Classes in certain files aren't generating CSS.

Solution: Use @source to explicitly include paths:

@import "tailwindcss";

@source "../node_modules/@your-company/ui-kit/src";
@source "./content/**/*.mdx";
Enter fullscreen mode Exit fullscreen mode

Problem 6: IDE Not Recognizing New Syntax

Solution: Update VS Code Tailwind CSS IntelliSense extension to the latest version. It fully supports v4 syntax including @theme and @plugin.

TypeScript Config Types (If You Still Need JS Config)

For complex configurations, you can still use a JS config file alongside CSS:

// tailwind.config.ts
import type { Config } from 'tailwindcss'

export default {
  // Minimal config - most things should be in @theme
  theme: {
    extend: {
      // Complex dynamic values that can't be CSS custom properties
    },
  },
} satisfies Config
Enter fullscreen mode Exit fullscreen mode

Then reference it:

@import "tailwindcss";
@config "./tailwind.config.ts";
Enter fullscreen mode Exit fullscreen mode

Performance Comparison: Real Numbers

Here's what we measured in a production Next.js app (500+ components):

Metric v3.4 v4.0 Change
Cold build 12.3s 1.8s 85% faster
Dev server start 4.2s 0.8s 81% faster
HMR update 340ms 12ms 96% faster
Production CSS 48KB 31KB 35% smaller
Memory usage 180MB 45MB 75% less

The Oxide engine is genuinely that much faster. If you're on a large codebase, the upgrade is worth it for performance alone.

Migration Checklist

Use this checklist for your migration:

  • [ ] Update dependencies (tailwindcss@latest, remove old packages)
  • [ ] Choose integration: Vite plugin, PostCSS, or CLI
  • [ ] Convert @tailwind directives to @import "tailwindcss"
  • [ ] Move tailwind.config.js theme to @theme in CSS
  • [ ] Update plugins to use @plugin syntax
  • [ ] Convert opacity classes to modifier syntax (/50)
  • [ ] Update any renamed utilities
  • [ ] Configure dark mode if using class strategy
  • [ ] Add @source for any non-standard content paths
  • [ ] Test all pages/components for visual regressions
  • [ ] Update VS Code extension
  • [ ] Remove old config files after confirming everything works

Should You Upgrade?

Upgrade now if:

  • Build times are painful (v4 is 5-10x faster)
  • You want smaller CSS bundles
  • You're starting a new project
  • You like the CSS-first configuration approach

Wait if:

  • You depend heavily on plugins that haven't been updated
  • You're in the middle of a critical release
  • Your tailwind.config.js has complex programmatic logic

For most projects, the upgrade takes 1-4 hours depending on config complexity. The performance gains are real Tailwind v4 is the smoothest CSS framework upgrade I've done in years.

Conclusion

Tailwind CSS v4 is a significant upgrade—new engine, new config paradigm, new defaults. The CSS-first approach with @theme feels strange at first but makes more sense the longer you use it. Configuration becomes inspectable in DevTools, IDE support improves, and your CSS file becomes the single source of truth.

The Oxide engine's performance improvements are not marketing hype. Hot reloads are genuinely instant. Cold builds are genuinely 5-10x faster. If you're working on a large project, that alone justifies the migration effort.

Start with a simple project to get comfortable with the new syntax, then tackle your main codebase. The migration isn't complicated—it's just different.

Happy styling.


🔒 Privacy First: This article was originally published on the Pockit Blog.

Stop sending your data to random servers. Use Pockit.tools for secure, client-side only utilities that keep your files 100% private.

Top comments (0)