Tailwind V4 Migration
Tailwind CSS v4 breaking changes, new features, and migration guide from v3 to v4
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 linesTailwind 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
| Area | v3 | v4 |
|---|---|---|
| Configuration | tailwind.config.js | CSS @theme directive |
| Engine | JavaScript (PostCSS) | Oxide (Rust-based) |
| Content detection | Manual content paths | Automatic source detection |
| Color palette | Named shades (50-950) | Same palette, renamed utilities |
| Default border color | gray-200 | currentColor |
@apply | Supported | Still supported but with caveats |
| Preflight | Included | Included with minor changes |
| Plugins | JS plugin() API | CSS-first @plugin directive |
| Dark mode | darkMode: '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/upgradehandles the majority of changes automatically. Review its output before making manual adjustments. - Migrate
tailwind.config.jsto@themeincrementally. Start with colors and fonts, verify, then move spacing, animations, and other tokens. - Use the Vite plugin when possible.
@tailwindcss/viteis 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
@utilitydirectives. - Keep
@themeblocks organized. Group related tokens (colors, typography, spacing, animations) with comments for maintainability.
Common Pitfalls
- Not removing
autoprefixer. v4 includes vendor prefixing. Keepingautoprefixerin the PostCSS config causes duplicate prefixes. - Leaving
tailwind.config.jsalongside@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-smtoshadow-xs,shadowtoshadow-sm) catches many developers off-guard. Search for these in your codebase. - Expecting
contentpaths to be required. Auto-detection works in most cases, but monorepo setups or projects importing components fromnode_modulesmay need explicit@sourcedirectives. - Using deprecated
@tailwinddirectives. 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
@themecan coexist during the transition, allowing you to verify each section before moving on. -
Keeping
autoprefixerin the PostCSS pipeline. Tailwind v4 handles vendor prefixing internally. Leavingautoprefixerinstalled produces duplicate prefixes, increases CSS output size, and slows builds for no benefit. -
Assuming utility names are unchanged. The shadow and blur renames (
shadow-smtoshadow-xs,shadowtoshadow-sm,blur-smtoblur-xs) are subtle and affect many components. Run a project-wide search for these utilities before considering migration complete. -
Deleting
tailwind.config.jsbefore verifying@themeequivalence. 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,
borderdefaulted togray-200. In v4, it defaults tocurrentColor. This single change can cause widespread visual regressions across every component that uses a plainborderclass without an explicit color.
Install this skill directly: skilldb add tailwind-skills
Related Skills
Tailwind Animations
Animation utilities, transitions, custom keyframes, and motion patterns with Tailwind CSS
Tailwind Component Patterns
Common UI component patterns including cards, navbars, forms, modals, and badges built with Tailwind CSS
Tailwind Custom Config
Customizing tailwind.config.js to extend themes, add custom colors, fonts, spacing, and configure content paths
Tailwind Dark Mode
Dark mode strategies including class-based toggling, media queries, and CSS variable theming with Tailwind CSS
Tailwind Fundamentals
Utility-first CSS fundamentals with Tailwind including class composition, spacing, typography, and layout primitives
Tailwind Plugins
Writing custom Tailwind CSS plugins to add utilities, components, base styles, and variants