Skip to main content
Adzbyte
DevelopmentTools

Tailwind v4 Migration: What Actually Changed

Adrian Saycon
Adrian Saycon
March 11, 20264 min read
Tailwind v4 Migration: What Actually Changed

I spent a weekend migrating two projects from Tailwind v3 to v4, and the experience was nothing like the v2-to-v3 jump. This time, the changes are structural. The entire configuration model shifted from JavaScript to CSS, and if you’re still using tailwind.config.js, you’re working against the framework instead of with it.

Here’s what actually changed and what tripped me up along the way.

The Big Shift: CSS-First Configuration

Tailwind v3 had you define your theme in tailwind.config.js — colors, fonts, spacing, all in a JavaScript object. In v4, that entire file is gone. Your theme lives in CSS using the @theme inline directive.

Before (v3 — tailwind.config.js):

module.exports = {
  theme: {
    extend: {
      colors: {
        primary: '#8139ff',
        accent: '#00c6d8',
      },
      fontFamily: {
        sans: ['Poppins', 'sans-serif'],
      },
    },
  },
};

After (v4 — globals.css):

@import "tailwindcss";

@theme inline {
  --color-primary: #8139ff;
  --color-accent: #00c6d8;
  --font-sans: 'Poppins', sans-serif;
}

Everything is now CSS custom properties under @theme inline. The inline keyword prevents Tailwind from generating separate custom property declarations — your values are inlined directly into the utilities. This is cleaner and avoids the cascade issues that plagued v3’s CSS variable approach.

Content Detection Is Automatic

Remember the content array in v3 where you listed every file path Tailwind should scan?

// v3 — gone in v4
content: ['./src/**/*.{js,ts,jsx,tsx}', './index.html']

Tailwind v4 uses automatic content detection. It scans your project intelligently without configuration. If you need to exclude specific paths, you use the @source directive in CSS, but for most projects, you don’t need to touch anything.

The New Color System

The default color palette got a refresh. Colors like blueGray and warmGray are gone — v4 uses a simplified naming convention. If you referenced specific v3 color values, you’ll need to check them. The shade numbers (50-950) still work, but the actual hex values shifted slightly.

The bigger change: custom colors are defined as CSS properties instead of nested objects. No more colors.brand.500 — it’s --color-brand-500.

Plugin System Overhaul

If you used custom plugins, this is where migration gets interesting. The v3 plugin API using addUtilities, addComponents, and matchUtilities still works through a compatibility layer, but the recommended approach is now pure CSS with @utility:

@utility content-auto {
  content-visibility: auto;
}

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

You define custom utilities right in your CSS. No JavaScript plugin file, no build step dependency. I converted about a dozen custom utilities this way and it took maybe fifteen minutes.

Gotchas I Hit

1. PostCSS config changes. Tailwind v4 ships its own PostCSS plugin. If you had tailwindcss and autoprefixer in your PostCSS config, you now only need @tailwindcss/postcss. Autoprefixer is built in.

// postcss.config.js — v4
module.exports = {
  plugins: {
    '@tailwindcss/postcss': {},
  },
};

2. @apply with custom utilities. If you used @apply with utilities defined in plugins, those need to be converted to CSS-based @utility definitions first. The compatibility layer doesn’t bridge this gap cleanly.

3. Dark mode configuration. The darkMode: 'class' config option is gone. Dark mode now uses the CSS @custom-variant directive if you need class-based toggling instead of prefers-color-scheme:

@custom-variant dark (&.dark);

4. Arbitrary value syntax. Still works, but I found a few edge cases where v3’s bracket syntax needed adjustment. Test your arbitrary values after migration.

The Migration Path

Tailwind provides an upgrade tool that handles most of the mechanical work:

npx @tailwindcss/upgrade

It rewrites your config into CSS @theme blocks and updates your PostCSS config. It handled about 80% of my migration automatically. The remaining 20% was custom plugins and a few edge cases with arbitrary values.

My advice: run the upgrade tool, then do a full visual regression check. The class names themselves are nearly identical, so your templates shouldn’t need changes. But the computed values might differ slightly, especially around colors and shadows.

Is It Worth Upgrading?

Yes. The CSS-first approach feels right. Your entire design system lives in one CSS file instead of being split between JavaScript config and CSS overrides. Build times are noticeably faster — the new engine (written in Rust via Oxide) compiles in milliseconds. And the simplified plugin model means fewer dependencies in your project.

If your project uses vanilla Tailwind without heavy plugin customization, budget a couple of hours. If you have custom plugins and complex theme extensions, budget a day. Either way, the result is a cleaner, faster setup.

Adrian Saycon

Written by

Adrian Saycon

A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.

Discussion (0)

Sign in to join the discussion

No comments yet. Be the first to share your thoughts.