Migrating from Tailwind CSS v3 to v4 — Complete Upgrade Guide
In this tutorial, you will learn about Migrating from Tailwind CSS v3 to v4. We cover key concepts, practical examples, and best practices to help you master this topic.
Migrating from Tailwind CSS v3 to v4 requires moving configuration from tailwind.config.js into CSS @theme blocks, updating the build tooling to Lightning CSS, and adapting variant and plugin usage.
What You'll Learn
You will learn the step-by-step migration Process from v3 to v4, how to convert your JavaScript config to CSS @theme, update build pipelines, and test for breaking changes.
Why It Matters
v4 offers faster builds, smaller bundles, and better DX. DodaTech migrated its entire component library in 2 hours, cutting build time from 3 seconds to 0.3 seconds.
Real-World Use
Doda Browser upgraded from v3 to v4 for its browser extension, reducing the config from 80 lines of JavaScript to 20 lines of CSS @theme directives.
flowchart LR
A[v3 Config] --> B[Migration]
B --> C[CSS @theme]
B --> D[Build Update]
B --> E[Plugin Update]
B --> F[Testing]
style B fill:#38bdf8,stroke:#0284c7,color:#fff
style C fill:#22c55e,stroke:#16a34a,color:#fff
Step 1: Update Packages
# Remove v3 packages
npm uninstall tailwindcss postcss autoprefixer @tailwindcss/postcss
# Install v4 packages
npm install tailwindcss @tailwindcss/vite
# If using CLI instead of Vite
npm install -g @tailwindcss/cli
Expected output: v4 packages installed. The old PostCSS-based pipeline is replaced.
Step 2: Convert Config to @theme
/* Before: tailwind.config.js (v3) */
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{html,js}'],
theme: {
extend: {
colors: {
brand: { 500: '#7c3aed', 600: '#6d28d9' },
},
fontFamily: {
display: ['Inter', 'sans-serif'],
},
},
},
darkMode: 'class',
plugins: [require('@tailwindcss/forms')],
}
/* After: main.css (v4) */
@import "tailwindcss";
@theme {
--color-brand-500: #7c3aed;
--color-brand-600: #6d28d9;
--font-family-display: "Inter", sans-serif;
}
/* Dark mode strategy */
@variant dark (&:where(.dark, .dark *));
Expected output: All v3 configuration converted to CSS @theme variables. The tailwind.config.js file can be removed.
Step 3: Update Build Setup
// Before: vite.config.js (v3)
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
css: {
postcss: './postcss.config.js',
},
})
/* After: vite.config.js (v4) */
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})
Expected output: Vite uses the @tailwindcss/vite plugin directly instead of PostCSS. Remove postcss.config.js.
Step 4: Adapt Custom Variants
/* Before: tailwind.config.js (v3 plugin variant) */
plugin(function({ addVariant }) {
addVariant('open', 'details[open] &');
})
/* After: main.css (v4 @variant) */
@variant open (&:where(details[open] &));
/* Usage unchanged */
<details class="open:bg-blue-50">
<summary>Toggle</summary>
Content
</details>
Expected output: Custom variants defined with @variant in CSS instead of JavaScript plugin. Usage in HTML remains the same.
Step 5: Update Plugins
/* Before: tailwind.config.js (v3) */
module.exports = {
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@tailwindcss/container-queries'),
],
}
/* After: main.css (v4) */
@import "tailwindcss";
@plugin "@tailwindcss/forms";
@plugin "@tailwindcss/typography";
/* Container queries are built-in in v4 - no plugin needed */
Expected output: Plugins use @plugin directive in CSS. Container queries are native in v4.
Step 6: Test and Verify
<!-- Test that common utilities still work -->
<div class="flex items-center justify-between p-4 bg-white dark:bg-gray-900 rounded-lg shadow">
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Migration Test</h2>
<button class="bg-brand-500 hover:bg-brand-600 text-white px-4 py-2 rounded transition-colors">
Button
</button>
</div>
Expected output: All standard utilities (flex, padding, colors, dark mode) work identically in v4. Custom theme values use the new CSS variable syntax.
Common Mistakes
1. Keeping tailwind.config.js
v4 ignores the JavaScript config file. Remove it after migrating all values to @theme in CSS.
2. Forgetting @plugin Directive
Old require() calls in plugins array do not work. Use @plugin "package-name" in the CSS file.
3. Not Updating Vite Config
Without @tailwindcss/vite plugin, Tailwind v4 does not process. Ensure smooth migration by updating vite.config.js.
4. Using Old Color Syntax
Custom colors in v4 use CSS variables: --color-brand-500: #7c3aed. The old dot-notation in JavaScript config does not apply.
5. Expecting PostCSS Plugins to Work
PostCSS autoprefixer and similar are handled by Lightning CSS. Remove them from the PostCSS config.
Practice Questions
How do you define custom colors in v4? Using CSS variables in @theme:
--color-brand-500: #7c3aed.What replaces the plugins array in v4? The CSS
@plugindirective:@plugin "@tailwindcss/forms".How do you define dark mode Strategy?
@variant dark (&:where(.dark, .dark *));in the CSS file.What build plugin replaces PostCSS for Vite?
@tailwindcss/vite-- import and add to the plugins array.Are container queries available in v4? Yes, built-in. The @tailwindcss/container-queries plugin is no longer needed.
Challenge
Take an existing v3 project and migrate it to v4. Convert the tailwind.config.js to CSS @theme, update Vite config, replace plugins with @plugin, and verify all components render correctly.
FAQ
Mini Project
Set up a fresh v4 project and migrate a v3 component library (3 components with custom colors, plugins, and dark mode). Document any differences you encounter during migration.
What's Next
With migration complete, explore CSS-First Configuration in depth. Then master Unified Variants for the new variant system.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro