BXUP Track progress & test yourself — free
Frontend & Mobile · Playbook · Core · ~4 min read
FRONTEND & MOBILE · PLAYBOOK · CORE

Migrate to Tailwind v4: Codemod the 80%, LLM the 20%

Tailwind ships an upgrade tool that handles the mechanical majority of a v4 migration. The remaining 20% — config-file logic, dynamic class construction, plugin replacements — is judgment work an LLM accelerates. The discipline is keeping the two lanes separate and gating every step behind pixel diffs, because style migrations fail silently.

Tier: Core · Workflow area: Styling, responsive & i18n

A migration PR that "looks fine" is the most dangerous artifact in frontend work: nothing crashes, nothing typechecks red, and the buttons are 2px smaller everywhere. This playbook builds the safety net first, then splits the work by what each tool is actually good at.

USE THIS WHEN

Hybrid is the right call

  • Migrating a real app from Tailwind v3 to v4 — new CSS-first @theme config, renamed utilities, new import.
  • Your tailwind.config.js carries custom themes, plugins, or JS logic that has no mechanical translation.
  • The codebase constructs class names dynamically (clsx, template strings, CVA variants) where codemods fear to tread.
SKIP IT WHEN

One lane is enough

  • A small or fresh project with default config — run the upgrade tool alone and eyeball it; this playbook is overhead.
  • No visual regression capability at all and no time to build one — do not start; an ungated migration of a large app is how you ship a subtly broken release.
  • You are actually migrating between systems (CSS Modules → Tailwind); that is a rewrite, not an upgrade.

Prerequisites

  • Green build, green tests, and a clean git branch — the codemod must be diffable in isolation.
  • A route/story inventory for screenshots: Storybook, or a list of representative URLs.
  • Node version matching the upgrade tool's requirement; browser targets compatible with v4 (it drops older browsers — check before you begin, not after).

The recipe

  1. Build the pixel gate before touching anything. Capture the "before" truth on the unmodified branch:

    npx playwright screenshot --viewport-size=390,844 http://localhost:3000/ shots/before/home-mobile.png
    

    In practice, write a small script iterating your route list at two or three viewports — or use Playwright's toHaveScreenshot() against a baseline run. Fifteen representative screens beat two perfect ones. Commit the baselines.

  2. Run the official upgrade tool in its own commit. The mechanical 80%:

    npx @tailwindcss/upgrade
    

    It migrates @tailwind directives to @import "tailwindcss", renames deprecated utilities (shadow-sm → shadow-xs, outline-none → outline-hidden, and friends), updates PostCSS/Vite wiring, and converts what it can of your config to CSS @theme. Commit the result untouched, labeled chore: tailwind v4 codemod (mechanical). Never mix hand edits into this commit — you want a pure diff you can trust and, if needed, revert.

  3. Screenshot-diff the codemod commit. Re-run the capture into shots/after-codemod/ and diff. Most diffs here will be genuinely intentional v4 rendering differences (default border color, ring width changes) — but each one gets classified, not waved through. Fix or accept, and record the accepted ones in the PR description.

  4. Hand the LLM the residue, with the diff as context. Now the 20%. Feed the agent three things: what the tool left behind (it prints warnings for what it could not migrate), your old tailwind.config.js, and the new CSS file:

    The Tailwind v4 upgrade tool ran; commit attached. Remaining work:
    1. tailwind.config.js still holds a plugin (typography), a custom
       spacing scale, and theme.extend colors referencing CSS vars.
       Convert to @theme in app.css. Show me the mapping table
       (old config key -> new @theme variable) before editing.
    2. Grep for dynamically built class names (clsx, cva, template
       literals) that reference renamed utilities — list every hit with
       file:line. Do NOT bulk-edit these; propose per-site fixes.
    Anything ambiguous, ask; do not guess a value.
    

    Dynamic class sites are the classic silent killer — shadow-${size} never matches a codemod's grep, and never crashes at runtime. The model is good at finding them; you decide the fix per site.

  5. Review the LLM lane like a design change, not a refactor. For each config translation, check the mapping table against rendered output, not just against the old config — theme.extend semantics and v4 defaults differ in places (border and ring colors are the notorious ones). Re-run the screenshot capture after this lane and diff again. This second gate catches what reading the diff cannot.

  6. Ship in slices if the app is big. v4's @import and v3 setups cannot coexist in one bundle, but you can still slice the review: land the codemod commit and LLM commits as separate PRs in quick succession, each with its own diff report, rather than one 400-file review nobody can hold in their head.

Warning

The upgrade tool requires a clean git state and a supported Node version, and v4 drops support for older browsers. Confirm your browser-support contract allows v4 before step 1 — discovering this in review means unwinding the whole branch.

Tool variations

  • Claude Code: run steps 4–5 as an agent loop with the screenshot script as its verification command — "edit, re-shoot, report the diff" per slice.
  • Cursor: strong for the per-site dynamic-class fixes; queue each grep hit as its own small edit with inline review.
  • Chromatic/Percy instead of raw Playwright: if you already have visual CI, use it as the gate — the baseline is your last production build, which is even better truth.
  • Same recipe, other migrations: styled-components → vanilla-extract, Sass → CSS Modules, MUI version bumps: mechanical tool or codemod first in a pure commit, LLM for the residue, screenshots as the gate throughout.

Quality check

  • Two clean lanesthe mechanical commit contains zero hand edits; every LLM-lane change traces to a named leftover or grep hit.
  • Config parity provenevery key from the old config appears in the mapping table as migrated, intentionally dropped, or flagged.
  • Dynamic sites enumeratedthe grep list for dynamic class construction exists in the PR, each hit resolved or exempted.
  • Pixel evidence attachedbefore/after diffs at each gate, with accepted intentional changes listed and everything else fixed.
  • No stragglersa final repo grep for deprecated utility names and @tailwind directives returns empty.
Key Takeaway:

Codemods are trustworthy and dumb; LLMs are clever and unreliable; screenshots are the only judge that cannot be fooled by either. Keep the three roles separate and a style migration becomes boring — which is exactly what you want a migration to be.

This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in