note-251109-tailwind-v4-migration
Tailwind CSS V4 Migration
Date: 2025-11-09
Issue: Build failures after upgrading to Tailwind v4
Status: ✅ Resolved
Problem
Build failed with error:
Cannot apply unknown utility class `text-gray-200`
Cannot apply unknown utility class `btn`
Root Cause
Tailwind v4 introduces breaking changes to how custom components and utilities are defined:
@layer componentsis deprecated - The old pattern of defining custom classes in@layer componentsno longer works- New
@utilitydirective - Custom utilities must use@utilityinstead of@layer utilities @applyscoping changes - In scoped contexts, need@referencedirective to access theme- CSS-first configuration - Theme customization now happens in CSS via
@theme, nottailwind.config.js
Key Learnings
V3 Pattern (Old)
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.btn {
@apply px-4 py-2 rounded;
}
}
V4 Pattern (New)
@import "tailwindcss";
@utility btn {
display: inline-flex;
align-items: center;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
/* ... */
}
OR for reusable components, define them in regular CSS without @utility, and apply utility classes directly in JSX/TSX.
Solution Applied
For WhatNext, we opted to:
- Remove custom component CSS - Delete
@layer componentsentirely - Use Tailwind utilities directly in JSX - Apply utility classes in React components instead of creating intermediate
.btn,.card, etc. classes - Keep theme configuration in
tailwind.config.js- v4 still supports JS config (on roadmap for stable)
Why This Approach?
- Simpler: No CSS abstraction layer to maintain
- More flexible: Easier to see exactly what styles are applied
- Better DX: IntelliSense works better with direct utility usage
- Aligned with v4 philosophy: Tailwind v4 encourages utility-first approach
Changes Made
- Updated
@import "tailwindcss"syntax - Removed
@layerusage from component styles - Updated
postcss.config.jsto use@tailwindcss/postcss - Components will use utility classes directly (e.g.,
className="btn-primary"becomesclassName="px-3 py-2 bg-primary-600 hover:bg-primary-500…")
References
Solution Found! ✅
The issue wasn't Vite 7 compatibility - it was using the wrong plugin!
The Problem
We were using @tailwindcss/postcss which had the "Missing field negated" error. Additionally:
- Multiple CSS files were importing Tailwind (fonts.css + components.css)
@importstatements were in wrong order
The Solution
-
Use
@tailwindcss/viteplugin instead of PostCSS plugin:npm install @tailwindcss/vite --save-dev --legacy-peer-deps -
Update
vite.config.ts:import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ plugins: [react(), tailwindcss()], }); -
Single CSS entry point (
src/styles/main.css):/* Font imports FIRST */ @import url('...'); /* Then Tailwind */ @import "tailwindcss"; /* Then custom utilities */ @utility btn { ... } -
Use
@utilitydirective for custom components (not@layer components)
Build Result
✓ 663 modules transformed.
✓ built in 11.19s
Status: ✅ Tailwind v4 working perfectly with Vite 7!
Key Learnings
- Plugin Choice Matters:
@tailwindcss/vite>@tailwindcss/postcssfor Vite projects - Import Order: External
@import→@import "tailwindcss"→ everything else - Single Entry Point: Only import Tailwind once in your main CSS file
@utilitySyntax: Define base styles, variants handled automatically via&:pseudo-selector- Persistence Pays Off: The official docs were right - v4 DOES work with Vite 7!
Next Steps
- Document compatibility solution
- Get build working with Tailwind v4
- Learn
@utilitydirective properly - Consider v4 theme configuration with
@themedirective for colors