Tailwind 4 in practice: what changes and when to migrate
I migrated 4 projects to Tailwind 4 over the last 60 days. Two on the day stable dropped. Two I waited 2 months and still broke things. This is the honest report: what actually changes, when migration is worth it, and when you shouldn’t migrate yet.
If you use Tailwind day to day, this post saves you about 6 hours of research and trial-and-error.
The real change is the engine
Tailwind 3 used PostCSS to process everything. Tailwind 4 uses Oxide, an engine written in Rust. That’s not a technical detail. It’s the difference between 2-3 second builds and 200ms builds on medium projects.
On my Astro project with around 250 components, hot reload time dropped from 1.8s to 180ms. The first time I saved a file and saw the browser refresh in nearly real time, I thought it was bugged. It wasn’t. It was Tailwind 4 working.
For small projects (10-20 components), the difference is imperceptible. For medium projects, it’s day and night. For anyone working in a large monorepo, it’s a game changer.
Smaller bundle, for real
The final CSS got smaller. Not 5%. 60-70% smaller in projects that use lots of variants.
Before, generated CSS had repetition and redundant rules. Tailwind 4 uses CSS layers natively, which reduces conflicting specificity and eliminates redundancy. On my site, CSS dropped from 24KB gzipped to 8KB.
For Lighthouse, that means faster LCP (less CSS to parse) and smaller bundle for mobile 4G. Measurable: bumped Performance score from 96 to 99 just from migration.
What changed in syntax
Here’s where migration pain lives. Some things work exactly the same, others broke silently.
@tailwind base/components/utilities became @import "tailwindcss". Single line. Cleaner.
tailwind.config.js is now optional. You can declare everything in CSS via @theme:
@import "tailwindcss";
@theme {
--color-brand-500: #2A9D7E;
--color-brand-600: #1F8267;
--font-sans: "Inter", sans-serif;
--spacing-section: 4rem;
}
For solo devs, that’s better. Everything design-system-related lives in CSS, together. No need to open 2 files.
@layer mostly disappeared. Tailwind 4 organizes everything in native CSS layers. You can still use @layer components if you want, but it’s no longer required.
The color rule changed. Before: bg-blue-500. Still works. BUT, if you defined colors.brand in the old config, in Tailwind 4 it becomes bg-brand-500 AND --color-brand-500 becomes a CSS variable automatically. You can use it anywhere with var(--color-brand-500).
Container queries built in
Tailwind 4 supports container queries natively. Before required a plugin.
<div class="@container">
<div class="@md:flex @md:gap-4">
Content that adapts to the container, not the viewport.
</div>
</div>
That changes layout for reusable components a lot. A card that fits in a small grid or a large space, without you needing to do JS-based media calc.
What broke in my projects
Custom typography plugin. Tailwind 4 changed the plugin API. I had to rewrite a plugin that added custom typography classes. Took 1 hour.
peer-checked variants. In some cases, the selector that worked in T3 doesn’t work in T4. Fix was to use new official variants. On an old project with complex forms, I had to adjust 6 inputs.
Color opacity syntax. bg-blue-500/50 still works, but bg-blue-500 bg-opacity-50 is deprecated. Anyone using the old style needs to update.
@apply in some contexts doesn’t behave like before. Specifically, @apply inside @layer base works differently. In my case, I had to change 3 global styles that used @apply for element resets.
When migration is worth it now
Worth it now if: new project (start from scratch), small-to-medium project (under 30k LOC), you’re active on the project (working on it for the next 6 months), and you use few custom plugins.
Not worth it now if: critical production project you haven’t touched in 2+ months, project with 5+ custom plugins, tight deadline in the next 30 days, or no staging environment.
Migration isn’t “run 1 command”. It’s “run the codemod, run build, see what broke, fix”. Reserve 4-6 hours for a medium project. For large, 1-2 days.
The official codemod
Tailwind has a codemod for automatic migration:
npx @tailwindcss/upgrade@latest
That command updates package.json, updates the config file, updates CSS, fixes deprecated classes. On a well-structured project, does 90% of the work.
The remaining 10% is custom plugins, regex that didn’t catch, and CSS that depended on Tailwind 3 specific behavior.
Always run on a separate branch. Always test everything manually. Always have a rollback plan.
Measured real performance
I benchmarked on 3 projects:
Project 1: Static Astro site (this blog).
- T3: build in 4.2s, final CSS 18KB
- T4: build in 1.1s, final CSS 7KB
Project 2: React management webapp (clinic).
- T3: dev server reload 1.6s, final CSS 38KB
- T4: dev server reload 220ms, final CSS 14KB
Project 3: Astro landing page.
- T3: build in 2.9s, final CSS 12KB
- T4: build in 800ms, final CSS 5KB
In all 3, dev productivity time changed substantially. The smaller final bundle improves Core Web Vitals across the board.
Framework compatibility
Astro: compatible since Astro 5. Configure via @astrojs/tailwind or direct via Vite plugin. Worked first try.
Next.js: compatible since Next 14. Setup via standard tailwindcss package. On the project I migrated, I had to update next.config.js to handle the new imports.
Vite + React: compatible directly. Vite official plugin handles it.
Remix/Nuxt/SvelteKit: good compatibility. Each framework has framework-specific migration docs.
Pure Webpack 5: works, but still has some warnings. Anyone still on pure Webpack 5 is probably on a legacy project and shouldn’t even migrate.
Changes that matter for design systems
Automatic CSS variables. Every token you define in @theme becomes a CSS variable automatically. That means you can use it outside Tailwind:
.custom-element {
background: var(--color-brand-500);
padding: var(--spacing-section);
}
No need to duplicate tokens. Single source of truth.
OKLCH colors. Tailwind 4 uses OKLCH internally, not HSL. Result: more consistent colors on modern wide-gamut screens. White-on-white is actually white, instead of slightly bluish.
Light/dark via light-dark(). Native CSS function works inside Tailwind 4.
Plugins that still make sense
Tailwind 4 absorbed many plugins that used to be external. Container queries, line-clamp, aspect-ratio are built-in now.
Still useful plugins: typography (for prose), forms (for form resets), animate (for complex animations with Framer Motion).
Obsolete plugins: container-queries-plugin, line-clamp-plugin, aspect-ratio-plugin. Can remove from package.json.
Migration checklist
- Create branch
tailwind-4-migration - Run
npx @tailwindcss/upgrade@latest - Check changes in package.json and tailwind.config.js
- Run build and check errors
- Test all components in dev and production
- Run Lighthouse before and after to measure gain
- Check if any custom plugin broke
- Update internal project documentation
- Deploy to staging first
- Keep T3 on another branch for 1-2 weeks for easy rollback
Migration pays off for most projects. But don’t run it under pressure. Reserve a day, take your time, test for real.
Read also: Design system with Tailwind | Astro 5: what’s new | Container queries in practice | Case Mariah
Is migration worth it?
For anyone working with sites and webapps day to day, yes. Dev productivity gain is immediate. Smaller bundle improves Core Web Vitals. New syntax is cleaner.
For anyone touching a project occasionally, wait. Tailwind 4 still has tweaks coming. In 6 months it’ll be more mature with more migration material.
The worst thing is migrating during a deadline. Migrating Tailwind requires test time. Without testing, you’ll find bugs in production at bad hours.
I migrated my active projects. Legacy projects I don’t touch will stay on T3 until I need to work on them. Deciding case by case is better than migrating everything at once.
Performance with Tailwind 4
Tailwind 4 generates less CSS than Tailwind 3 (better optimization). Stylesheet size dropped 15-20% on my projects. Load time improved slightly.
JIT compilation is faster too. Rebuild time when editing is now sub-100ms on most projects (was 300-500ms in Tailwind 3).
If you’re still on Tailwind 3, upgrade. Performance gains alone are worth the effort.
Debugging Tailwind 4
Sometimes a class doesn’t apply. You write bg-red-500 and it doesn’t change the background.
First check: is the class spelled right? bg-red-5000 (typo) won’t work.
Second check: is the selector too low specificity? p bg-red-500 might lose to body { background: blue }.
Third check: is it in a media query context that doesn’t apply?
Use browser DevTools to inspect. If Tailwind class doesn’t appear in the stylesheet at all, you spelled it wrong. If it appears but is overridden, specificity issue.
Tailwind 4 debugging is the same as Tailwind 3: use DevTools and look for the class in the stylesheet.
Themes and dark mode
Tailwind 4 makes themes easier. Instead of storing colors as SCSS variables, use CSS variables through Tailwind.
For dark mode: define variables in :root (light mode) and override them in @media (prefers-color-scheme: dark).
Tailwind picks up these variables automatically. Your theme switches without any JavaScript. Browser preference is respected.
This is simpler than previous versions and production-grade.