Migrating from Tailwind v3 to v4: What Actually Changed

Bryan Heath Bryan Heath
· · 12 min read

Tailwind CSS v4 is the biggest architectural shift the framework has seen since its initial release. The core idea is simple: move configuration out of JavaScript and into CSS. But the implications touch nearly every file in your project. This guide covers what actually changed, what you need to update, and the gotchas that will trip you up if you are not paying attention.

The Big Shift: JavaScript Config to CSS-First

In Tailwind v3, everything revolved around tailwind.config.js. Your colors, fonts, spacing, plugins, content paths — all lived in a JavaScript file that Tailwind's build process consumed at compile time.

Tailwind v4 replaces this entirely with a CSS-first approach. Instead of a JS config file, you configure Tailwind directly in your CSS using native CSS features: @import, @theme, and @plugin directives.

Here is what your main CSS file looks like now:

/* v3: You had three directives */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* v4: One import does it all */
@import "tailwindcss";

That single @import "tailwindcss" statement replaces all three @tailwind directives. It pulls in the base styles, components layer, and utilities layer in one shot. The tailwind.config.js file? You can delete it.

Migrating Your Theme with @theme

The @theme directive is where your custom design tokens live now. Everything you previously defined in the theme or theme.extend section of your JS config moves here as CSS custom properties.

Before (v3):

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        mint: {
          500: '#3EB489',
          600: '#35997A',
          700: '#2C7F6B',
        },
      },
      fontFamily: {
        heading: ['Inter', 'sans-serif'],
        body: ['Source Sans Pro', 'sans-serif'],
      },
      spacing: {
        '18': '4.5rem',
        '88': '22rem',
      },
    },
  },
};

After (v4):

@import "tailwindcss";

@theme {
  --color-mint-500: #3EB489;
  --color-mint-600: #35997A;
  --color-mint-700: #2C7F6B;

  --font-heading: 'Inter', sans-serif;
  --font-body: 'Source Sans Pro', sans-serif;

  --spacing-18: 4.5rem;
  --spacing-88: 22rem;
}

The naming convention follows a predictable pattern: the Tailwind namespace becomes the CSS custom property prefix. Colors become --color-*, fonts become --font-*, spacing becomes --spacing-*, border radii become --radius-*, and so on. Once defined in @theme, the corresponding utility classes are automatically generated. --color-mint-500 gives you bg-mint-500, text-mint-500, border-mint-500, and every other color utility automatically.

A critical detail: values defined in @theme replace the defaults for that namespace. If you define --color-mint-500 without also defining the standard palette colors, you will lose them. To extend instead of replace, use @theme inline or define only the keys you want to add.

Deprecated Utilities and Their Replacements

Tailwind v4 cleaned house on several utility patterns. Here are the ones most likely to break your existing templates:

Opacity modifiers. The old bg-opacity-*, text-opacity-*, and border-opacity-* utilities are gone. Use the slash syntax instead:

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

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

Flex and grid shorthand changes. Several utilities were renamed to align with their CSS properties more directly:

<!-- v3 -->
<div class="flex-grow-0 flex-shrink">...</div>
<div class="decoration-slice">...</div>
<div class="overflow-clip">...</div>

<!-- v4 -->
<div class="grow-0 shrink">...</div>
<div class="box-decoration-slice">...</div>
<div class="text-clip">...</div>

Shadow and ring color defaults. In v3, ring defaulted to a semi-transparent blue. In v4, the default ring color is currentColor. If your design relied on the old blue ring default, you will need to explicitly set ring-blue-500/50 wherever you used the bare ring utility.

Border color default. The border utility no longer defaults to gray-200. It now uses currentColor, matching standard CSS behavior. Audit any bare border usage and add an explicit color if needed.

Plugin Changes: The @plugin Directive

In v3, plugins were loaded through the plugins array in your JS config. In v4, you load them with the @plugin directive directly in CSS:

/* v3: in tailwind.config.js */
/* plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms')] */

/* v4: in your CSS file */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";

Note that official Tailwind plugins have been updated for v4 compatibility. Third-party plugins may require updates from their maintainers. Check each plugin's documentation before migrating.

Content Detection: No More Content Array

One of the most welcome changes in v4 is the removal of the content array. In v3, you had to explicitly tell Tailwind which files to scan for class names:

// v3: tailwind.config.js
module.exports = {
  content: [
    './resources/**/*.blade.php',
    './resources/**/*.js',
    './resources/**/*.vue',
  ],
  // ...
};

In v4, Tailwind automatically detects your source files. It scans your project directory, respects your .gitignore, and finds template files without any configuration. If you need to add additional sources outside the default detection, use the @source directive:

@import "tailwindcss";
@source "../vendor/my-package/resources/views";

This is especially relevant for monorepos or projects where classes are generated in packages outside your main source tree.

Renamed and Removed Utilities

Beyond the deprecations above, several other utilities changed names to be more consistent or were removed entirely. Here is a quick reference:

The flex-grow and flex-shrink utilities are now simply grow and shrink. The decoration-clone and decoration-slice utilities are now box-decoration-clone and box-decoration-slice. The blur and backdrop-blur-0 utilities now use blur-none and backdrop-blur-none. The shadow utility without a size suffix now maps to a slightly different default value, so verify your shadows visually after upgrading.

Container queries, which required a plugin in v3, are now built into v4 natively with @container support. You can remove @tailwindcss/container-queries from your dependencies.

PostCSS and Vite Config Changes

Tailwind v4 ships with a new dedicated Vite plugin, which replaces the old PostCSS-based setup for Vite projects:

// v3: vite.config.js
export default defineConfig({
  plugins: [
    laravel({ input: ['resources/css/app.css', 'resources/js/app.js'] }),
  ],
});

// v3: postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};
// v4: vite.config.js
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [
    laravel({ input: ['resources/css/app.css', 'resources/js/app.js'] }),
    tailwindcss(),
  ],
});

// postcss.config.js: autoprefixer is no longer needed
// You can delete this file if Tailwind was the only reason it existed

The @tailwindcss/vite plugin handles everything that the PostCSS plugin used to, plus it integrates more tightly with Vite's hot module replacement. Autoprefixer is now built in, so you can remove it from your dependencies. If you are not using Vite, a @tailwindcss/postcss plugin is available as a drop-in replacement for the old tailwindcss PostCSS plugin.

Migration Checklist

Here is the step-by-step order that has worked well across multiple project migrations:

  1. Run the official upgrade tool. Tailwind provides npx @tailwindcss/upgrade which handles many of the mechanical changes. Run it first and review the diff.

  2. Update dependencies. Install tailwindcss@4 and @tailwindcss/vite (or @tailwindcss/postcss). Remove autoprefixer and the old tailwindcss PostCSS plugin.

  3. Replace your CSS entry point. Swap the three @tailwind directives for @import "tailwindcss".

  4. Migrate your theme. Move colors, fonts, spacing, and other design tokens from tailwind.config.js into a @theme block in your CSS.

  5. Move plugins to @plugin directives. Replace the plugins array with @plugin statements in CSS.

  6. Delete the config file. Once everything is migrated, remove tailwind.config.js and postcss.config.js (if no longer needed).

  7. Update Vite config. Add the @tailwindcss/vite plugin to your vite.config.js.

  8. Search and replace deprecated utilities. Do a project-wide search for bg-opacity, text-opacity, flex-grow, flex-shrink, and the other renamed utilities. Replace them with the v4 equivalents.

  9. Check default color changes. Audit bare border and ring usage for color regressions.

  10. Visual regression test. Build your project and compare every page. The subtle default changes (shadows, borders, rings) are the ones that slip through code-level audits.

Gotchas That Will Trip You Up

A few things that caught us off guard during migration:

The @apply directive still works, but its behavior with custom utilities defined in @theme can be surprising. If you reference a theme token in @apply, make sure it is defined before the @apply rule in your stylesheet.

If you use darkMode class strategy in v3, know that v4 uses the @custom-variant directive for custom dark mode selectors. The media query strategy works out of the box, but class-based dark mode now requires explicit configuration.

Arbitrary values and arbitrary properties still work the same way with square bracket notation, so bg-[#1a1a2e] and [mask-type:luminance] are unchanged. However, the theme() function in CSS is deprecated. Use var(--color-mint-500) directly instead of theme(colors.mint.500).

Prefix support changed too. In v3 you set prefix: 'tw-' in the config. In v4, use @import "tailwindcss" prefix(tw) in your CSS. The prefix no longer uses a dash separator by default.

Conclusion

Tailwind v4 is a fundamental rethinking of how the framework is configured, not just an incremental update. The shift from JavaScript configuration to CSS-first configuration makes your design system more portable, more inspectable, and more aligned with how CSS actually works. Custom properties defined in @theme are real CSS variables that you can access anywhere — in your stylesheets, in JavaScript, even in dev tools.

The migration is not trivial, especially on larger codebases. But the official upgrade tool handles the bulk of the mechanical work, and the remaining changes are straightforward find-and-replace operations. The biggest time sink will be visual regression testing — verifying that subtle default changes have not shifted your UI in unexpected ways.

Start with the upgrade tool, work through the checklist above, and budget an afternoon for a thorough visual review. Once you are on v4, the developer experience improvements — faster builds, simpler configuration, and native CSS integration — make the effort worth it.

Share:

Related Posts