Tailwind v4 is the largest change to the project since utilities were introduced. The Oxide engine rewrites the class scanner, the configuration model moves from tailwind.config.js into CSS, and a small but real list of utilities renamed, consolidated, or were removed outright.
For most teams the runtime upgrade is mechanical. The risky part is the layer of tooling that grew on top of v3 — the ESLint config, the editor plugins, the AI agents that write Tailwind from training data still anchored in v3 syntax. Those do not migrate themselves. This post is a punch-list for the parts that bite.
What actually changes for the linter
Three categories of change matter for any tool that inspects Tailwind classes — your ESLint plugin, your editor, and any AI agent generating markup against your tokens.
1. Configuration moves into CSS
v3 read its scale, theme, and plugin list from a JS module. v4 reads from a CSS file via @theme and friends:
/* app/styles.css — v4 */
@import "tailwindcss";
@theme {
--color-brand-primary: #1A5276;
--color-brand-primary-dark: #173F62;
--spacing-18: 4.5rem;
--font-display: "Satoshi", system-ui;
}Any linter that read your token scale from tailwind.config.js — including the older versions of eslint-plugin-tailwindcss and most internal rule wrappers — now reads from a file that is empty. Your "valid colours" allowlist silently becomes the empty set, every class in your codebase becomes legal, and every off-token hex an agent generates becomes invisible.
2. A small set of utilities renamed
v4 cleaned up some long-running inconsistencies. The shadow, blur, and rounded scales gained an -xs tier; the old -sm aliases shifted; opacity utilities like bg-opacity-50 are deprecated in favour of the slash syntax bg-black/50.
| v3 | v4 | Notes |
|---|---|---|
| shadow-sm | shadow-xs | Old shadow-sm reassigned to a heavier value. |
| rounded-sm | rounded-xs | Same scale shift; existing rounded-sm usages render larger. |
| bg-opacity-50 | bg-black/50 | Slash syntax is the canonical form. The old *-opacity-* family still works in v4.0 but is deprecated. |
| flex-shrink-0 | shrink-0 | Aliases consolidated; both still emit but the canonical is shorter. |
| space-x-4 / space-y-4 | gap-4 (preferred) | Still emitted, but flex/grid gap is the recommended pattern. |
These shifts are mostly safe at the runtime layer — but a stale ESLint rule that hard-codes the v3 names will either flag valid v4 code, or worse, miss the deprecated form entirely. AI coding agents trained primarily on v3 corpora produce them by default.
3. The Oxide engine's scan surface
v4's class extraction is faster and stricter. Class strings built dynamically with template literals (`bg-${shade}`) are no longer guaranteed to be discovered. v3 lint rules that relied on the same template-literal heuristic stop matching what the runtime actually compiles. The fix is the one Tailwind has recommended for years — only ever pass full class names as strings — but the migration window is when the divergence surfaces.
The migration in five steps
Most projects can run the official codemod and ship the upgrade in one PR. The steps below assume an existing v3 setup with ESLint, Prettier, and at least one Tailwind plugin enabled.
- 01
Run the codemod
npx @tailwindcss/upgrade@latestThis rewrites
tailwind.config.jsinto a CSS@themeblock, swaps the renamed utilities in your source files, and updates your dependencies. Read the diff — do not blindly accept it. The codemod is conservative with custom plugins. - 02
Replace the ESLint plugin
eslint-plugin-tailwindcssthrough v3.x targets the v3 class set. If you depended on itsno-custom-classnameorclassnames-orderrules, pin a v4-compatible release or move to a tool that reads from CSS@themedirectly. Either way the install line changes:# remove pnpm remove eslint-plugin-tailwindcss # install — pick one pnpm add -D eslint-plugin-tailwindcss@next # community v4 track pnpm add -D @deslint/eslint-plugin # reads tokens from @theme - 03
Re-import your tokens into the lint config
This is the step most teams forget. The codemod migrates the runtime token source to CSS, but your linter still needs to know what is allowed. Mirror the values declared inside your
@themeblock into thedesignSystemsection of.deslintrc.json— colors, spacing, radii, fonts. If you also publish a Style Dictionary or Stitch token file as part of your build,npx deslint import-tokenscan pull from there directly:npx deslint import-tokens --style-dictionary ./tokens/build/tokens.jsonOnce the allowlist matches your
@themeexactly,no-arbitrary-colors,no-arbitrary-spacing, andno-arbitrary-typographystop drifting against the new runtime. - 04
Sweep the renamed utilities
The codemod handles the obvious cases. Anything generated after the codemod ran — a stray PR opened against the v3 branch, an AI agent with v3 priors — will keep producing
bg-opacity-*,shadow-smat the old weight, andflex-shrink-0. A targeted lint sweep catches them:# deslint flags v3 classes that drift back in npx deslint scanThe same
no-conflicting-classesrule that catchesflex hiddenalso catches the v3/v4 cohabitation patterns — for exampleshadow-sm shadow-mdfrom a half-applied rename. - 05
Lock the gate in CI
The migration is only complete when v3 patterns can no longer enter the codebase. Wire the deslint scan into your PR check — it runs in seconds, has zero cloud dependency, and the budget gate halts the merge if drift returns:
# .github/workflows/lint.yml — relevant step - name: Deslint run: npx @deslint/cli scan --budget .deslint/budget.yml
The bit nobody talks about: AI agents are still on v3
Most coding agents' training corpora skew heavily toward v3 Tailwind. That means even after your runtime is on v4, your Claude Code, Cursor, Codex, and Windsurf sessions will keep generating v3-shaped class strings: the old shadow-sm weight, the deprecated bg-opacity-* family, the tailwind.config.js file you just deleted.
The fix is the same fix you have for any context-poor generation: hand the agent a deterministic checker as a tool. When deslint runs as an MCP server, the agent calls analyze_and_fix before it commits — and the v3 patterns get rewritten into their v4 equivalents the same wayno-arbitrary-colors rewrites a hex into a token. No prompt engineering, no guessing.
What the agent sees on each call
{
"rule": "no-conflicting-classes",
"file": "components/Card.tsx",
"line": 14,
"message": "shadow-sm shifted in Tailwind v4. Use shadow-xs for the old weight.",
"fix": { "from": "shadow-sm", "to": "shadow-xs" }
}Three commands to verify the migration
# 1. install the v4-aware lint set
pnpm add -D @deslint/eslint-plugin @deslint/cli
# 2. update designSystem in .deslintrc.json to mirror your @theme block
# (or pull from a Style Dictionary build with import-tokens)
# 3. measure
npx deslint coverageWant the v4 check inside the AI loop?
The CLI tells you what drifted. The MCP server tells the agent before it writes. Same v4-aware rule set, single stdio subprocess, zero cloud.
Related reading