Skip to main content
Technology & EngineeringTailwind332 lines

Tailwind V4 Migration

Tailwind CSS v4 breaking changes, new features, and migration guide from v3 to v4

Quick Summary29 lines
You are an expert in migrating projects from Tailwind CSS v3 to v4 and leveraging v4's new capabilities.

## Key Points

- **Run the official migration tool first.** `npx @tailwindcss/upgrade` handles the majority of changes automatically. Review its output before making manual adjustments.
- **Migrate `tailwind.config.js` to `@theme` incrementally.** Start with colors and fonts, verify, then move spacing, animations, and other tokens.
- **Use the Vite plugin when possible.** `@tailwindcss/vite` is faster than the PostCSS plugin because it integrates directly with Vite's pipeline.
- **Test border colors after migration.** The default border color change is the most common source of visual regressions.
- **Audit third-party plugins.** Not all v3 plugins have v4 support. Check for updates or replace with CSS `@utility` directives.
- **Keep `@theme` blocks organized.** Group related tokens (colors, typography, spacing, animations) with comments for maintainability.
- **Not removing `autoprefixer`.** v4 includes vendor prefixing. Keeping `autoprefixer` in the PostCSS config causes duplicate prefixes.
- **Leaving `tailwind.config.js` alongside `@theme`.** Both can work together during migration, but conflicting values cause confusion. Remove the JS config once migration is complete.
- **Assuming shadow/blur utility names are unchanged.** The shift (`shadow-sm` to `shadow-xs`, `shadow` to `shadow-sm`) catches many developers off-guard. Search for these in your codebase.
- **Expecting `content` paths to be required.** Auto-detection works in most cases, but monorepo setups or projects importing components from `node_modules` may need explicit `@source` directives.
- **Using deprecated `@tailwind` directives.** Replace `@tailwind base; @tailwind components; @tailwind utilities;` with a single `@import "tailwindcss";`.
- **Forgetting that `darkMode: 'class'` moved to CSS.** In v4, configure dark mode with `@custom-variant dark (&:where(.dark, .dark *));` in your CSS file instead of the JS config.

## Quick Example

```css
@import "tailwindcss";
@source "../node_modules/my-ui-lib/src";
```

```bash
npx @tailwindcss/upgrade
```
skilldb get tailwind-skills/Tailwind V4 MigrationFull skill: 332 lines
Paste into your CLAUDE.md or agent config

Tailwind v4 Changes and Migration — Tailwind CSS

You are an expert in migrating projects from Tailwind CSS v3 to v4 and leveraging v4's new capabilities.

Overview

Tailwind CSS v4 is a ground-up rewrite that replaces the JavaScript-based configuration with a CSS-first approach. The engine is built on Oxide (a Rust-based core) for dramatically faster builds. Configuration moves from tailwind.config.js into CSS using @theme directives. While most utility classes remain the same, the configuration model, plugin system, and some defaults have changed significantly.

Core Philosophy

Tailwind v4 represents a philosophical shift from JavaScript-centric configuration to CSS-first authoring. The @theme directive replaces tailwind.config.js because design tokens are fundamentally a CSS concern — they define visual properties, and CSS is where visual properties belong. This is not just a syntax change; it is a recognition that the configuration file was an intermediary that added complexity without proportional value for most projects.

The move to Oxide (the Rust-based engine) reflects a commitment to developer experience through raw performance. Faster builds mean tighter feedback loops, which means developers are more willing to experiment and iterate. Automatic content detection removes another source of configuration friction — you no longer need to manually maintain a list of file paths, and new files are picked up immediately without restarting the dev server.

Migration should be incremental and tool-assisted. The official @tailwindcss/upgrade CLI handles the mechanical transformations, and the JS config can coexist with @theme during the transition. Rushing to delete tailwind.config.js before understanding the new paradigm causes avoidable regressions. Migrate colors and fonts first, verify, then move spacing and animations, and finally remove the JS config once everything is confirmed working.

Core Concepts

What Changed in v4

Areav3v4
Configurationtailwind.config.jsCSS @theme directive
EngineJavaScript (PostCSS)Oxide (Rust-based)
Content detectionManual content pathsAutomatic source detection
Color paletteNamed shades (50-950)Same palette, renamed utilities
Default border colorgray-200currentColor
@applySupportedStill supported but with caveats
PreflightIncludedIncluded with minor changes
PluginsJS plugin() APICSS-first @plugin directive
Dark modedarkMode: 'class' config@variant dark or @custom-variant

CSS-First Configuration with @theme

In v4, design tokens are defined directly in CSS:

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

@theme {
  --color-brand-50: #eff6ff;
  --color-brand-500: #3b82f6;
  --color-brand-900: #1e3a5f;

  --font-family-sans: "Inter", system-ui, sans-serif;
  --font-family-mono: "JetBrains Mono", monospace;

  --breakpoint-xs: 475px;
  --breakpoint-3xl: 1920px;

  --spacing-4\.5: 1.125rem;
  --spacing-18: 4.5rem;

  --animate-fade-in: fade-in 0.3s ease-out;

  @keyframes fade-in {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: translateY(0); }
  }
}

This replaces the entire tailwind.config.js for most projects.

Automatic Content Detection

v4 no longer requires explicit content paths. It automatically scans your project for template files, respecting .gitignore. You can still configure paths if needed:

@import "tailwindcss";
@source "../node_modules/my-ui-lib/src";

New Import Syntax

/* v3 */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* v4 */
@import "tailwindcss";

Implementation Patterns

Running the Migration Tool

Tailwind provides an automated migration tool:

npx @tailwindcss/upgrade

This handles most mechanical changes: updating config to CSS @theme, renaming deprecated utilities, and adjusting imports.

Migrating tailwind.config.js to @theme

Before (v3):

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          500: '#6366f1',
          600: '#4f46e5',
        },
      },
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
      },
      borderRadius: {
        '4xl': '2rem',
      },
    },
  },
}

After (v4):

@import "tailwindcss";

@theme {
  --color-brand-500: #6366f1;
  --color-brand-600: #4f46e5;
  --font-family-sans: "Inter", sans-serif;
  --radius-4xl: 2rem;
}

Renamed and Removed Utilities

<!-- v3 → v4 renames -->
<!-- bg-opacity-50 → bg-black/50 (opacity modifier syntax, already available in v3.1+) -->
<div class="bg-black/50">Use opacity modifier</div>

<!-- shadow-sm → shadow-xs, shadow → shadow-sm (shifted naming) -->
<div class="shadow-xs">Was shadow-sm in v3</div>
<div class="shadow-sm">Was shadow in v3</div>

<!-- blur-sm → blur-xs, blur → blur-sm (same pattern) -->
<div class="blur-xs">Was blur-sm in v3</div>

<!-- ring-opacity-*, divide-opacity-* → use opacity modifier -->
<div class="ring-blue-500/50">Ring with opacity</div>

<!-- decoration-slice/clone → box-decoration-slice/clone -->
<div class="box-decoration-slice">Updated prefix</div>

Default Border Color Change

In v3, borders defaulted to gray-200. In v4, the default is currentColor:

<!-- v3: this had a gray border -->
<div class="border">Implicit gray border</div>

<!-- v4: explicitly set the color -->
<div class="border border-gray-200">Explicit gray border</div>

The migration tool adds explicit border colors where the default was relied upon.

Migrating Plugins

v3 JavaScript plugin:

const plugin = require('tailwindcss/plugin')
module.exports = {
  plugins: [
    plugin(function ({ addUtilities }) {
      addUtilities({
        '.scrollbar-hidden': {
          '-ms-overflow-style': 'none',
          'scrollbar-width': 'none',
          '&::-webkit-scrollbar': { display: 'none' },
        },
      })
    }),
  ],
}

v4 CSS plugin approach:

@import "tailwindcss";

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

For complex plugins, v4 still supports a JavaScript plugin API via @plugin:

@import "tailwindcss";
@plugin "./plugins/my-plugin.js";

Custom Variants in v4

@import "tailwindcss";

@custom-variant dark (&:where(.dark, .dark *));
@custom-variant sidebar-open (.sidebar-open &);

Handling CSS Variable Themes (shadcn/ui)

The CSS variable theming pattern works well in v4 with @theme:

@import "tailwindcss";

@theme {
  --color-background: hsl(var(--background));
  --color-foreground: hsl(var(--foreground));
  --color-primary: hsl(var(--primary));
  --color-primary-foreground: hsl(var(--primary-foreground));
  --color-muted: hsl(var(--muted));
  --color-muted-foreground: hsl(var(--muted-foreground));
  --color-border: hsl(var(--border));
  --radius-lg: var(--radius);
  --radius-md: calc(var(--radius) - 2px);
  --radius-sm: calc(var(--radius) - 4px);
}

Package Changes

# v3
npm install tailwindcss postcss autoprefixer

# v4 — PostCSS plugin
npm install tailwindcss @tailwindcss/postcss

# v4 — Vite plugin (preferred for Vite projects)
npm install tailwindcss @tailwindcss/vite

Vite configuration:

// vite.config.js
import tailwindcss from '@tailwindcss/vite'

export default {
  plugins: [
    tailwindcss(),
  ],
}

PostCSS configuration (v4):

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

Note: autoprefixer is no longer needed — v4 handles vendor prefixing internally.

Next.js Migration

// postcss.config.mjs (Next.js with v4)
const config = {
  plugins: {
    '@tailwindcss/postcss': {},
  },
}
export default config

Remove tailwind.config.js after migrating tokens to @theme in your CSS file.

Best Practices

  • Run the official migration tool first. npx @tailwindcss/upgrade handles the majority of changes automatically. Review its output before making manual adjustments.
  • Migrate tailwind.config.js to @theme incrementally. Start with colors and fonts, verify, then move spacing, animations, and other tokens.
  • Use the Vite plugin when possible. @tailwindcss/vite is faster than the PostCSS plugin because it integrates directly with Vite's pipeline.
  • Test border colors after migration. The default border color change is the most common source of visual regressions.
  • Audit third-party plugins. Not all v3 plugins have v4 support. Check for updates or replace with CSS @utility directives.
  • Keep @theme blocks organized. Group related tokens (colors, typography, spacing, animations) with comments for maintainability.

Common Pitfalls

  • Not removing autoprefixer. v4 includes vendor prefixing. Keeping autoprefixer in the PostCSS config causes duplicate prefixes.
  • Leaving tailwind.config.js alongside @theme. Both can work together during migration, but conflicting values cause confusion. Remove the JS config once migration is complete.
  • Assuming shadow/blur utility names are unchanged. The shift (shadow-sm to shadow-xs, shadow to shadow-sm) catches many developers off-guard. Search for these in your codebase.
  • Expecting content paths to be required. Auto-detection works in most cases, but monorepo setups or projects importing components from node_modules may need explicit @source directives.
  • Using deprecated @tailwind directives. Replace @tailwind base; @tailwind components; @tailwind utilities; with a single @import "tailwindcss";.
  • Forgetting that darkMode: 'class' moved to CSS. In v4, configure dark mode with @custom-variant dark (&:where(.dark, .dark *)); in your CSS file instead of the JS config.

Anti-Patterns

  • Big-bang migration. Attempting to migrate an entire large codebase from v3 to v4 in a single PR is high-risk. Migrate incrementally — the JS config and @theme can coexist during the transition, allowing you to verify each section before moving on.

  • Keeping autoprefixer in the PostCSS pipeline. Tailwind v4 handles vendor prefixing internally. Leaving autoprefixer installed produces duplicate prefixes, increases CSS output size, and slows builds for no benefit.

  • Assuming utility names are unchanged. The shadow and blur renames (shadow-sm to shadow-xs, shadow to shadow-sm, blur-sm to blur-xs) are subtle and affect many components. Run a project-wide search for these utilities before considering migration complete.

  • Deleting tailwind.config.js before verifying @theme equivalence. The migration tool handles most transformations, but custom plugins, complex presets, and conditional configuration logic may not convert cleanly. Keep the JS config as a reference until every token and plugin has been manually verified in the CSS-first setup.

  • Ignoring the default border color change. In v3, border defaulted to gray-200. In v4, it defaults to currentColor. This single change can cause widespread visual regressions across every component that uses a plain border class without an explicit color.

Install this skill directly: skilldb add tailwind-skills

Get CLI access →