Skip to content

Tailwind v4 Migration: What Broke When I Deleted My Config File

Tailwind v4 Migration: What Broke When I Deleted My Config File

I put off the Tailwind v4 migration for longer than I’d like to admit. The release shipped, everyone posted hot takes, and I kept clicking “remind me later” on my own mental Jira board. Last weekend I finally bit the bullet and upgraded three real projects: a client dashboard, a side project, and a Laravel app carrying four years of accumulated CSS guilt.

Short version for the impatient: the official upgrade tool handles most of the work, and the parts it can’t handle are the ones that quietly change how your app looks. No build errors. No red text in the terminal. Just a focus ring that’s suddenly thinner and borders that shifted to a different gray while nobody was watching.

This is the migration guide I wish I’d read first. Not a feature tour, more of a field report on what broke, what didn’t, and what I’d check before trusting the diff.

Run the upgrade tool before you read anything else

My first mistake was reading blog posts for two hours before touching code. The right move is the opposite. Tailwind ships a codemod that does the heavy lifting:

npx @tailwindcss/upgrade

Run it on a fresh branch with a clean git status, because it touches a lot of files. On my client dashboard it updated the dependencies, converted tailwind.config.js into CSS, rewrote the directives in my stylesheet, and renamed the utilities that changed in v4. That project took about twenty minutes, and fifteen of those were me reading the diff with growing suspicion that it couldn’t possibly be this easy.

The official upgrade guide is what you read second, when the diff shows something you don’t understand. It covers every removed utility and changed default in one page. I had it open the whole weekend.

One warning: the tool needs Node 20 or later, and it refuses to run on a dirty working tree. That refusal is a feature. Commit first.

Your config file moves into your CSS

This is the change everyone talks about, and it’s real: tailwind.config.js is no longer the center of your Tailwind universe. In v4, configuration lives in your stylesheet. Here’s a v3 setup I had:

// tailwind.config.js (v3)
module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {
      colors: {
        brand: "#2563eb",
      },
      fontFamily: {
        display: ["Satoshi", "sans-serif"],
      },
    },
  },
};

And the v4 equivalent:

/* app.css (v4) */
@import "tailwindcss";

@theme {
  --color-brand: #2563eb;
  --font-display: "Satoshi", sans-serif;
}

The three @tailwind directives are gone too. @tailwind base, @tailwind components, and @tailwind utilities collapse into that single @import "tailwindcss" line.

Two things I like about this after a week of living with it. First, every theme token becomes a real CSS variable at runtime, so var(--color-brand) works anywhere, including in third-party widgets and inline styles where Tailwind classes couldn’t reach. Second, the content array is gone. v4 detects your template files automatically, with heuristics built around your .gitignore. I was skeptical. It found everything, including the weird Blade templates in the Laravel project that my old glob patterns kept missing.

If you have a genuinely complicated JS config, @config "./tailwind.config.js" still works as an escape hatch. I used it on the Laravel app and plan to unwind it slowly rather than in one heroic commit.

The renamed utilities that changed my UI without asking

Here’s where the weekend got interesting. Several defaults changed in ways that compile fine and look wrong.

The one that got me: ring. In v3, bare ring gave you a 3px ring. In v4 it’s 1px. Every focus state in the dashboard got subtly worse, and I only noticed because a screenshot diff flagged it.

<!-- v3: this was a 3px ring -->
<button class="ring ring-blue-500">Save</button>

<!-- v4: be explicit if you want the old look -->
<button class="ring-3 ring-blue-500">Save</button>

The shadow scale shifted down a step as well. v3’s shadow-sm is now shadow-xs, and bare shadow became shadow-sm. The upgrade tool renames these for you, but any classes built dynamically in JavaScript, string concatenation, CMS content, that sort of thing, are invisible to it. Grep for those by hand.

Also worth knowing: outline-none was renamed to outline-hidden (the new outline-none actually sets outline: none), and the default border color changed from gray-200 to currentColor. That last one made a few of my dividers turn nearly black. The fix is boring: specify a color, like border-gray-200, wherever you relied on the old default.

None of these are bugs. They’re better defaults for new projects. But “better default” and “silent visual regression” are the same thing wearing different hats.

Browser support is the real go or no-go decision

v4 targets Safari 16.4+, Chrome 111+, and Firefox 128+. It leans on modern CSS features like @property and color-mix(), so this floor is structural, not a suggestion.

Before migrating the client project, I pulled the browser stats. About 1.2% of sessions were on older Safari builds, mostly aging iPads. We decided that was acceptable because the site degrades to readable content rather than breaking outright. Your math may differ. An internal tool for a company that standardizes on current Chrome? Migrate today. A public site with meaningful traffic from old devices? Check your analytics first, and stay on v3.4 without shame if the numbers say so. v3 didn’t stop working the day v4 shipped.

The features that made it worth the Saturday

I don’t want this to read like a complaint list, because the payoff is real. Builds are much faster on the new engine. The v4 announcement post claims up to 5x faster full builds and far bigger gains on incremental rebuilds, and my experience matches: the dashboard’s dev server rebuilds went from “noticeable” to “did it even run?”.

Container queries are built in now, no plugin required. I wrote about fixing an actual layout bug with them in my container queries post, and they’ve quietly become my favorite thing in v4. Dark mode also got easier to control; I covered my setup in the dark mode flash post if you’re wiring that up.

And because every token is a CSS variable, design system work gets simpler. The client dashboard now themes itself per tenant by swapping a handful of variables, something that took a genuinely embarrassing amount of JavaScript before. That project is the same kind of work I show on my portfolio if you’re curious what I build day to day.

What I’d actually do this week

If you’re sitting on a v3 project, here’s the move. Make a branch. Run npx @tailwindcss/upgrade. Read the whole diff, not just the file names. Then grep your codebase for shadow-sm, ring, outline-none, and bare border classes that came from string concatenation, because the codemod can’t see those. Take before-and-after screenshots of your five most important screens and compare them at actual size.

Budget half a day for a medium project. Mine averaged out to about three hours each, and most of that was verification, not fixing. The migration itself is the easy part. Trusting it is what takes the time.