v3 → v4 migration

[check: …]
📖 Pair this live demo with the companion guide (.md) — this page is the rendered ground truth (it proves the v4 renamed utilities compile via the Play CDN). ↗ Builds on preflight & reset (Preflight v4 changes ship with this migration) and build tooling (the PostCSS / Vite / Lightning pipeline you upgrade alongside).

v3 → v4: config → CSS-first

v4 moves the source of truth out of tailwind.config.js and into your CSS. Theme tokens become CSS variables under @theme, the three @tailwind directives collapse to one @import, several utilities were renamed for consistency, the default border color is now currentColor, and dark mode is a one-line @custom-variant. The automated codemod (npx @tailwindcss/upgrade) rewrites ~90% of this — but it can't catch semantic mistakes (a bare border that now inherits text color), so you still need the table below.

areav3 (JS config / old syntax)v4 (CSS-first)
config file tailwind.config.js (required) optional — @config bridges a legacy file; most tokens move into @theme
directives @tailwind base; @tailwind components; @tailwind utilities; @import "tailwindcss";
theme tokens theme.extend.colors.brand @theme { --color-brand: #06b6d4; }
content / source content: [...] array auto-detect + @source directive (see source detection)
dark mode darkMode: 'class' @custom-variant dark (&:where(.dark, .dark *));
default border color gray-200 currentColor
opacity shorthand text-black opacity-50 text-black/50 (slash works for any color util)
variant stacking applied right-to-left (innermost-first) applied left-to-right (outermost-first) — the literal order you read

0 · live proof — the renamed v4 utilities compile

This page loads the real Tailwind v4 Play CDN. The two cells below are styled only by v4-renamed utilities. The gold-check at the top of the page runs getComputedStyle() on them to prove the renames map correctly: bg-linear-to-r must produce a real linear-gradient(...), and a bare border must resolve its color to currentColor (the text color), not gray-200.

class: bg-linear-to-r from-cyan-400 to-blue-500 (was bg-gradient-to-r)

class: border + text-cyan-400 (border default = currentColor now)

border = my text color

grad.background-image: — · border.borderColor: —

1 · interactive migration checker — pick a category

Click a category button. The left column shows v3 code (what the codemod rewrites from); the right shows the v4 equivalent (what it rewrites to). Every entry is something npx @tailwindcss/upgrade handles automatically — except where flagged in the gotchas panel.

v3 — before

// (selecting a category loads code here)

v4 — after

// (selecting a category loads code here)

2 · renamed utilities — the full table

These are mechanical renames the codemod does for you. The why column is the consistency rule behind each rename. (Any v3 name still in your code after the codemod is a bug — verify with node --check on your build or a visual diff.)

v3v4why
bg-gradient-to-r bg-linear-to-r new bg-linear-* + bg-radial-* + bg-conic-* family — names match the CSS function
bg-gradient-to-br etc. bg-linear-to-br etc. same family rename — all bg-gradient-*bg-linear-*
flex-grow grow shorter; mirrors the CSS property flex-grow → utility root grow
flex-shrink shrink shorter; same rule as grow
overflow-ellipsis text-ellipsis it's a text-* property (text-overflow), not an overflow one
decoration-clone text-decoration-clone matches the CSS property text-decoration-color family
decoration-slice text-decoration-slice same family
shadow-sm shadow-xs whole shadow scale shifts down by one to make room for a new shadow-sm
shadow shadow-sm same scale shift
rounded-sm rounded-xs same scale-shift logic on the radius scale
rounded rounded-sm same scale shift
outline-none outline-hidden outline-none now means outline-style: none; the old "force-transparent" behavior is outline-hidden
ring (3px default) ring-3 default ring width changed from 3px to 1px; old default is now explicit ring-3
blur-sm etc. blur-xs etc. blur scale shifts down by one (same as shadow/radius)

Pattern: v4 introduced finer-grained low ends on the shadow, rounded, blur, and drop-shadow scales. To make room, every existing step was renamed one tier down — so shadow-sm becomes shadow-xs, shadow becomes shadow-sm, and so on. This is the rename most likely to silently change your design if you skip the codemod.

3 · the automated upgrade tool

The official codemod rewrites your config, CSS, and templates in place. Commit a clean tree first — it edits files on disk and you'll want to review the diff before keeping anything.

upgrade — shell
# 1. install the v4 packages (replaces tailwindcss v3) $ npm install tailwindcss@latest @tailwindcss/postcss@latest # 2. run the codemod on a CLEAN git tree (it edits files in place) $ git commit -am "chore: snapshot before v4 upgrade" $ npx @tailwindcss/upgrade # ── output (abridged) ────────────────────────────────────────── Upgraded tailwind.config.js → moved theme tokens into @theme Rewrote @tailwind base/components/utilities@import "tailwindcss" Renamed bg-gradient-to-*bg-linear-to-* (24 files) Renamed flex-grow/flex-shrinkgrow/shrink (9 files) Shifted shadow/rounded/blur scale (1 step down) Manual review needed: bare border classes now use currentColor Manual review needed: darkMode: 'class' moved to @custom-variant Done. Review with: git diff

What it does for you: package swap, config → @theme token migration, directive rewrite, every utility rename in the table above, and the content:[] → auto-detection shift. What it can not decide for you: whether a now-currentColor border was a design choice or an accident, and any custom PostCSS / Vite wiring outside Tailwind itself.

1
Install v4npm install tailwindcss@latest @tailwindcss/postcss. Swap the PostCSS plugin from tailwindcss to @tailwindcss/postcss.
2
Commit clean — the codemod edits in place; you need a baseline to diff against.
3
Run npx @tailwindcss/upgrade — interactive prompt; it walks the config + every source file.
4
Review git diff — especially border colors, the shadow/radius scale shift, and dark mode.
5
Delete the old config if everything moved into @theme; otherwise keep it and bridge with @config "./tailwind.config.js";.

4 · killer gotchas — what the codemod can't catch

Bare border now uses currentColor. In v3 an unqualified border rendered gray-200; in v4 it inherits the element's text color. The codemod does NOT add a color — it can't tell whether the gray was intentional. Fix: grep for \bborder\b in templates and add border-gray-200 (or whatever the design calls for) where the color mattered.
The shadow / rounded / blur scales shifted down one tier. shadow-sm is now smaller than it was — the codemod renames mechanically, but if you had hand-tuned shadow values, the visual will subtly change. Fix: spot-check hero sections and cards after the upgrade.
ring default width changed from 3px to 1px. A bare ring in v3 drew a 3px ring; in v4 it's 1px. The codemod rewrites it to ring-3 to preserve the width — but if you relied on the default without writing ring (e.g. focus styles from a UI lib), the ring gets thinner. Fix: audit focus rings across forms and buttons.
outline-none changed meaning. In v3 it set a transparent 2px outline (forced, for accessibility during forced-colors mode); in v4 outline-none sets outline-style: none (the literal CSS meaning). The old behavior moved to outline-hidden. Fix: the codemod rewrites this, but verify keyboard focus is still visible.
Variant stacking direction is now left-to-right. md:hover: still means "hover within the md breakpoint", but in v3 the rightmost variant was applied first; in v4 the leftmost is. For 2-variant stacks this is identical, but for 3+ variants (focus:hover:md:) the order matters. Fix: review any 3+ variant stacks; the codemod does not rewrite them.
The @config bridge is a temporary crutch. If you keep tailwind.config.js and bridge with @config "./tailwind.config.js";, plugins and presets loaded via JS still work — but new v4 features (@theme inline, CSS-only @utility) won't see those tokens. Fix: use the bridge to ship the migration, then move tokens into @theme in a follow-up PR.

intent → pattern (cheat sheet)

intentv3 patternv4 pattern
import Tailwind @tailwind base; @tailwind components; @tailwind utilities;@import "tailwindcss";
define a brand color theme.extend.colors.brand = "#06b6d4" @theme { --color-brand: #06b6d4; }
class-based dark mode darkMode: "class" @custom-variant dark (&:where(.dark, .dark *));
scan extra source paths content: ["./src/**/*"] @source "../src"; (auto-detect by default)
linear gradient bg-gradient-to-r from-cyan-400 to-blue-500 bg-linear-to-r from-cyan-400 to-blue-500
grow / shrink flex-grow / flex-shrink grow / shrink
50% opacity black text text-black opacity-50 text-black/50
visible focus ring, 3px ring ring-3
small shadow shadow-sm shadow-xs (or shadow-sm for the new small)
keep a legacy config (n/a) @config "./tailwind.config.js"; at top of CSS