Web Development · Frontend
Tailwind CSS v4: What Changed and How to Migrate
Tailwind v4 moves configuration from JavaScript to CSS, drops the content array, and ships a faster engine. Here's what the breaking changes actually mean for a real project migration.
Anurag Verma
6 min read
Sponsored
Tailwind CSS v4 was a bigger rethink than the jump from v2 to v3. The JavaScript configuration file is gone. The content array is gone. The @tailwind directives are gone. If you pick up the v4 docs without reading the migration guide first, it looks like a different library.
The underlying utility-first philosophy is the same. The ergonomics are better once you’ve made the switch. But the migration has real friction, and understanding why the decisions were made makes it easier to adapt your existing setup.
What Actually Changed
CSS-first configuration. Tailwind v3 required a tailwind.config.js file for custom colors, fonts, spacing, and everything else. In v4, all customization happens inside your CSS file using a @theme directive.
Automatic content detection. v3 required you to list every file path Tailwind should scan for class names in the content array. v4 scans your project automatically using heuristics. In most projects you don’t need to configure this at all.
New import syntax. Instead of three @tailwind directives (base, components, utilities), you use one CSS import:
@import "tailwindcss";
Lightning CSS as the underlying engine. v4 uses Lightning CSS for parsing and transforms, which improves speed and adds native support for modern CSS features like nesting and the color-mix function.
CSS variables everywhere. All design tokens are now exposed as CSS custom properties by default. Your theme colors become --color-blue-500, spacing values become --spacing-4, etc.
Setting Up a New Project
npm install tailwindcss @tailwindcss/vite
The Vite plugin is now the recommended integration for most frameworks:
// vite.config.ts
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
tailwindcss(),
],
});
Your main CSS file becomes:
/* src/app.css */
@import "tailwindcss";
That’s the entire setup for a new project. No config file. No content paths. Run your build tool and the utilities are available.
Customizing the Theme
In v3, custom colors went in tailwind.config.js:
// v3 — tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: {
500: '#3b82f6',
600: '#2563eb',
},
},
},
},
};
In v4, the same customization goes in your CSS:
/* v4 — app.css */
@import "tailwindcss";
@theme {
--color-brand-500: #3b82f6;
--color-brand-600: #2563eb;
--font-display: "Inter", sans-serif;
--spacing-18: 4.5rem;
--radius-xl: 0.75rem;
}
These variables are automatically available as Tailwind utilities: bg-brand-500, text-brand-600, font-display, p-18, rounded-xl.
Because they’re CSS variables, you can override them at runtime without rebuilding:
/* Dark mode or different theme */
[data-theme="dark"] {
--color-brand-500: #60a5fa;
}
Removing Values from the Default Theme
If you want to remove a default color or spacing value to prevent it from being used:
@theme {
--color-red-*: initial; /* removes all red-* colors */
--spacing-px: initial; /* removes the px spacing value */
}
The initial keyword tells v4 to remove that token entirely.
Breaking Changes That Affect Most Projects
@apply with variants. In v3, @apply supported arbitrary combinations. v4 is stricter; some @apply patterns that worked before need to be rewritten. The common one:
/* v3 — works */
.btn {
@apply hover:bg-blue-600 focus:ring-2;
}
/* v4 — works */
.btn {
@apply hover:bg-blue-600 focus:ring-2;
}
Most @apply usage still works. The cases that break involve complex stacked variant combinations. Run the migration tool to find them.
Default border color. In v3, border applied a gray border by default. In v4, border adds a border but uses currentColor. If you have border classes without an explicit color, the border color will match the text color. Add explicit border colors to fix: border border-gray-200.
Ring defaults. The default ring width changed. ring in v4 may produce a different size than v3. Check your focus styles.
bg-opacity-* and text-opacity-* utilities removed. Use the slash syntax instead:
<!-- v3 -->
<div class="bg-blue-500 bg-opacity-50">
<!-- v4 -->
<div class="bg-blue-500/50">
Renamed utilities. A small set of utilities were renamed for consistency:
| v3 | v4 |
|---|---|
shadow-sm | shadow-xs |
shadow | shadow-sm |
rounded | rounded-sm |
blur | blur-sm |
If you’ve been using these without suffixes, expect visual changes.
The Official Migration Tool
Tailwind ships a codemod that handles the mechanical parts:
npx @tailwindcss/upgrade
Run this in your project root. It:
- Rewrites your
tailwind.config.jsinto@themeblocks in your CSS - Updates
@tailwinddirectives to@import "tailwindcss" - Converts opacity utilities to slash syntax
- Flags patterns it can’t automatically fix
After running it, do a visual pass through your UI. The tool handles the renames and import changes, but the default color and ring changes need manual verification.
Content Detection in Practice
v4’s automatic scanning works well for standard project structures. It scans files it can find through your imports and build graph.
If you have unusual file locations (e.g., content in a sibling directory, or HTML in a directory not reachable from your entry point), you can add explicit source paths:
@import "tailwindcss";
@source "../content/**/*.html";
@source "../../packages/ui/src/**/*.tsx";
This is additive, not a replacement for auto-detection.
Custom Plugins
v3 plugins used a JavaScript API. Most plugins still work in v4 because v4 maintains the plugin API for compatibility. But if you have local plugins that use addBase, addComponents, or addUtilities, they continue to work:
// Still works in v4 (in a separate config file)
import type { Config } from 'tailwindcss';
export default {
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms'),
],
} satisfies Config;
If you use the config file approach alongside v4, import it from your CSS:
@import "tailwindcss";
@config "./tailwind.config.js";
This lets you migrate incrementally: move what you can to @theme blocks and keep complex plugin configuration in the JS file until you’re ready.
Is It Worth Migrating Now?
For projects in active development: yes, migrate. The auto-detection reduces setup friction for new developers, the CSS variable exposure makes theming cleaner, and the Lightning CSS engine is measurably faster for large projects.
For stable projects in maintenance mode: the migration is safe but not urgent. The v3 codemod is available, but if the UI is working and you’re not actively adding new features, there’s no compelling reason to take on the visual regression risk.
New projects should default to v4. The setup is simpler than v3, the customization model is better, and you won’t carry the mental overhead of the old config format.
The biggest conceptual shift is accepting that your design tokens live in CSS, not JavaScript. Once that clicks, the rest of v4 follows naturally.
Sponsored
More from this category
More from Web Development
CSS Anchor Positioning: Tooltips and Popovers Without JavaScript
gRPC in 2026: When to Use It Instead of REST or GraphQL
k6 Load Testing: Performance Testing Your APIs Before Users Find the Problems
Sponsored
The dispatch
Working notes from
the studio.
A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored