I migrated a project to Tailwind v4 last week. The first hour was disorienting — half of what I knew didn't apply. Two days in, I'm faster than I was on v3.
These are the five features that made the difference.
1. @theme is the new config
In v3, every theme value lived in a JavaScript object inside tailwind.config.js. Custom colors, fonts, radii — all there.
In v4, the config moved into your CSS:
@import "tailwindcss";
@theme {
--color-background: #0a0a0a;
--color-foreground: #fafafa;
--color-accent: #06b6d4;
--font-sans: "Inter", system-ui, sans-serif;
}
These become real CSS custom properties. --color-accent works in two places at once: in raw CSS (color: var(--color-accent)) and in Tailwind classes (bg-accent, text-accent).
I don't miss tailwind.config.js.
2. @custom-variant for class-based dark mode
Built-in dark mode in v4 keys off the prefers-color-scheme media query. If you want class-based dark mode (so users can toggle themes manually), v3 had a config option. V4 wants this:
@custom-variant dark (&:where(.dark, .dark *));
Then everywhere in your code, dark:bg-card activates when .dark class is on <html>. Pair it with a CSS override block:
.dark {
--color-background: #0a0a0a;
--color-foreground: #fafafa;
}
Toggle by adding/removing the class. No more config-file dance.
3. color-mix() for accent-aware effects
This isn't strictly a Tailwind feature — it's CSS — but Tailwind v4 made it feel native by encouraging CSS-first variables.
The use case: a glow effect that tracks your accent color, regardless of theme.
.glow {
box-shadow:
0 6px 20px -6px color-mix(in srgb, var(--color-accent) 55%, transparent);
}
Change --color-accent, the glow color follows. Change theme (dark to light), the glow stays accent-aware. One declaration, two-mode behavior.
I use this everywhere — buttons, cards, focus rings. It's a tiny detail that polishes a whole UI.
4. Container queries without a plugin
In v3, @tailwindcss/container-queries was a plugin you installed. In v4, container queries are built in.
<div className="@container">
<div className="grid grid-cols-1 @md:grid-cols-2 @lg:grid-cols-3">
{/* responds to container width, not viewport */}
</div>
</div>
The grid switches columns based on the parent's width, not the window. If you've ever built a dashboard widget that's used at different sizes in different layouts, this is the cleanest solution I've found.
5. CSS-first config means less plumbing
I had a folder for Tailwind plugin configs in v3 projects. In v4 it doesn't exist.
Want a custom utility?
@utility container-tight {
max-width: 36rem;
margin-inline: auto;
}
Use it: <div className="container-tight">.
Want a custom variant?
@custom-variant hocus (&:hover, &:focus);
Use it: <button className="hocus:bg-accent">.
No more plugin(function({ addUtilities, addVariant }) { … }). The mental model collapses into "CSS with extra rules", which is what Tailwind probably should have been all along.
What I'd skip
Honestly, the @apply directive. It exists, it works, but I find that with CSS variables driving everything, I reach for it less. Direct class composition wins for clarity 80% of the time, and the other 20% I reach for @layer components { … }.
Migration tip
The official upgrade tool (npx @tailwindcss/upgrade@latest) handles 90% of the rewrite. The 10% it doesn't: dark-mode setup (you have to choose: media query or class), and any custom plugins (rewrite them as @utility or @custom-variant).
I spent 30 minutes on the upgrade tool, then another 90 minutes on manual cleanup. Net result: less code, the same UI, and a project that's easier to onboard.
If you've been holding off because v3 works fine — try v4 on a small project first. The mental shift is real, but it's a one-time cost and the daily ergonomics are worth it.
One last thing
The biggest win I didn't expect: @theme makes design tokens visible. In v3 they lived inside a JS object that nobody on the team looked at. In v4 they're at the top of your globals.css, next to the imports. New contributors find them in their first 30 seconds in the file.
That's not a Tailwind feature. That's a CSS architecture decision that v4 quietly forces on you. And it's a good one.
Top comments (0)