--- id: a-tokyo/agent-skills/tailwind-v3-to-v4-migration version: "c9d1a19e" license: MIT install: manual updated: 2026-07-26 --- # tailwind-v3-to-v4-migration — Automates the mechanical parts of upgrading from Tailwind v3 to v4, then handles the judgment calls the codemod can't: reconciling dependencies, porting config to CSS-first `@theme`, auditing v4's changed defaults (border, ring, placeholder, cursor, dialog, hover), and verifying the rendered output stays identical. Works across Next.js, Vite, Tailwind CLI, and plain PostCSS setups. Publisher: a-tokyo · Stars: 14 · Updated: 2026-07-26 Install (manual): `git clone https://github.com/a-tokyo/agent-skills` ## SKILL.md # tailwind-v3-to-v4-migration Upgrade a codebase from Tailwind CSS v3 to v4. The codemod does ~80% of the mechanical work; this skill supplies the 20% of judgment where migrations actually break — changed defaults, config porting, plugin/animation swaps, and proving nothing moved. ## When to use - Upgrading any project from Tailwind v3.x to v4.x. - Build errors after a partial upgrade: `@tailwind` directives unknown, missing `@tailwindcss/postcss`, `Cannot apply unknown utility class`, `tailwind.config` no longer picked up. - Converting `tailwind.config.{js,ts}` to CSS-first `@theme`. Skip if: the project is already on v4; you need to *downgrade*; or you only need a brand-new design system (use `tailwind-design-system`). Note v4 targets **Safari 16.4+, Chrome 111+, Firefox 128+** — if you must support older browsers, stay on v3.4 (flag this to the operator before proceeding). ## The one idea that makes this safe **A correct migration is a visual no-op.** Every renamed utility is a pure alias — `shadow-sm`→ `shadow-xs`, `rounded`→`rounded-sm`, `ring`→`ring-3`, `outline-none`→`outline-hidden` all compile to the *same* CSS as before. So what changes pixels is almost entirely v4's **changed defaults** (Step 3); the few non-default exceptions — the `space-x/y-*` & `divide-*` selector change, gradient-variant preservation, and `container` config removal — are flagged in Step 4. Rename mechanically, neutralize the changed defaults, fix those few exceptions, and the rendered output is identical. That is also how you verify success (Step 5): capture the UI before, prove it's unchanged after. ## Procedure Always work on a branch. Run the steps in order; do not skip Step 0 or Step 3. ### Step 0 — Pre-flight & baseline (do not skip) 1. Confirm Node 20+ (`node -v`) and that the working tree is clean. Create a branch (e.g. `tailwind-v4`). 2. **Inventory** every Tailwind entry point — there may be more than one: each CSS file with `@tailwind`/`@import "tailwindcss"`, every `tailwind.config.*`, every `postcss.config.*`, the bundler config (next/vite/webpack), and `package.json`. Monorepos: do this per package. 3. Record the current setup: `darkMode` value, custom `theme.extend`, `plugins`, the package manager (npm/yarn/pnpm/bun), and two easy-to-miss config options that need special handling later: **`prefix`** (v4 changes `tw-flex`→`tw:flex`) and **`theme.container`** (`center`/`padding` are gone in v4 — recreate via `@utility container`). 4. **Capture a baseline of how the app looks now** so you can prove the migration changed nothing: a screenshot set or a visual-regression run on v3 (see `references/05-verification-playwright.md`), or at minimum a list of key pages to eyeball. Confirm the project builds green on v3 first. ### Step 1 — Run the official upgrade tool ```bash npx @tailwindcss/upgrade@latest # clean git tree required… npx @tailwindcss/upgrade@latest --force # …or pass --force if untracked/uncommitted files exist ``` The tool refuses to run on a dirty tree (so you can review its diff). Commit/stash unrelated changes, or use `--force`. It updates dependencies, migrates the config to CSS where it can, rewrites `@tailwind` directives, and codemods most renamed/removed utilities in templates. **Review the full diff** — it is a starting point, not the finish line. If it errors (offline, exotic setup, unsupported config), fall back to the manual path in `references/01-breaking-changes.md` + `references/02-css-first-config.md` and continue. **Monorepos:** run the tool once per package root and confirm `tailwindcss` resolves to 4.x in *every* package's `node_modules` — a half-migrated workspace compiles some packages against v3. ### Step 2 — Reconcile dependencies & build plumbing Verify the tool did these; finish any it missed (`references/04-framework-setups.md` for your stack): - **Deps:** remove `tailwindcss@3`; add `tailwindcss@^4`. Remove `autoprefixer` and `postcss-import` (v4 does prefixing + import inlining itself). - **PostCSS:** `postcss.config.*` → `{ plugins: { '@tailwindcss/postcss': {} } }` (add the `@tailwindcss/postcss` dep). **Vite:** prefer `@tailwindcss/vite` over PostCSS. **CLI:** `npx tailwindcss` → `npx @tailwindcss/cli`. - **CSS entry:** `@tailwind base/components/utilities;` → `@import "tailwindcss";`. - **Plugins:** delete now-built-in ones (`@tailwindcss/container-queries`, `@tailwindcss/aspect-ratio`, line-clamp) — and remove their dead `theme`/usage. **`@tailwindcss/typography` stays** but is loaded in CSS via `@plugin "@tailwindcss/typography";` and must be bumped to a v4-compatible release (≥0.5.16). - **`container` customization:** if v3 set `theme.container.center`/`padding`, those options are gone — recreate as `@utility container { margin-inline: auto; padding-inline: 2rem; }` or every `container` loses its centering/padding silently. - Reinstall with the project's package manager so the lockfile updates; the `tailwindcss` version must resolve to 4.x. ### Step 3 — Changed-defaults audit + compat shims (the parity killers) These changed defaults are the main thing that moves pixels (see Step 4 for the few non-default exceptions). Walk the checklist; for each "relied on", paste the shim into your main CSS (after `@import "tailwindcss";`). Full rationale in `references/03-compat-shims.md`. - [ ] **Border/divide color** is now `currentColor` (was `gray-200`). If you use bare `border`/`divide` without a color anywhere, add: ```css @layer base { *, ::after, ::before, ::backdrop, ::file-selector-button { border-color: var(--color-gray-200, currentColor); } } ``` - [ ] **Ring** is now 1px / `currentColor` (was 3px / `blue-500`). Replace bare `ring`→`ring-3`; if you relied on the blue default add `ring-blue-500`. (Compat-only escape: `@theme { --default-ring-width: 3px; --default-ring-color: var(--color-blue-500); }`.) - [ ] **Placeholder** is now current text @ 50% (was `gray-400`). To keep v3 look: ```css @layer base { input::placeholder, textarea::placeholder { color: var(--color-gray-400); } } ``` - [ ] **Buttons** now use `cursor: default` (was `pointer`): ```css @layer base { button:not(:disabled), [role="button"]:not(:disabled) { cursor: pointer; } } ``` - [ ] **``** margins are reset (was centered): `@layer base { dialog { margin: auto; } }` if needed. - [ ] **Hover** now applies only on `(hover: hover)` devices. If your UI depends on tap-to-hover, add `@custom-variant hover (&:hover);`. - [ ] **Dark mode:** if v3 used `darkMode: 'class'` (or a custom selector), add `@custom-variant dark (&:is(.dark, .dark *));`. If it used `'media'`, v4's default already matches — **do nothing** (adding the class variant would *break* media-driven dark mode). > **Two of these are invisible to a screenshot harness:** the **button-cursor** and **hover-on-tap** > shims change behavior, not painted pixels, so visual parity (Step 5) can't confirm them. Decide them > by reasoning about the markup (do real `