SuperAPI AI Directory

tailwind-errors-v4-migration

Frontend & websites

View repository

Updated 10-15 22:18

About this skill

v3 to v4 Upgrade Trap Catalog

Every documented breaking change between Tailwind v3 and v4 with symptom, root cause, surgical fix, and (where applicable) a @layer base shim that restores v3 behavior during staged migration.

ALWAYS run the official codemod first :

npx @tailwindcss/upgrade

The codemod handles ~80% of the renames mechanically. This skill catalogs what it does NOT auto-fix, plus what to expect when visually-equivalent classes silently change meaning.

Companion skills :

  • tailwind-impl-migration-v3-v4 : end-to-end migration process
  • tailwind-impl-config-v4 : target v4 config surface
  • tailwind-impl-config-v3 : source v3 config surface

Quick Reference

Top 10 traps

Trap v3 form v4 form
Variant stacking order first:*:pt-0 *:first:pt-0
Default border color gray-200 currentColor
Default ring width 3px 1px
Shadow scale shift shadow-sm shadow-xs
Opacity modifier removal bg-opacity-50 bg-black/50
Prefix syntax tw-flex tw:flex
Important modifier !flex flex!
Outline rename outline-none outline-hidden
Arbitrary CSS var bg-[--brand] bg-(--brand)
Arbitrary comma in grid [max-content,auto] [max-content_auto]

Removed config options

Removed Replacement
corePlugins @utility for custom utilities ; no general off-switch
safelist @source inline "{...}" in CSS
separator No replacement ; v4 uses : only
@tailwind directives @import "tailwindcss"

Removed utilities

Removed Replacement
flex-shrink-* shrink-*
flex-grow-* grow-*
overflow-ellipsis text-ellipsis
decoration-slice box-decoration-slice
decoration-clone box-decoration-clone

Decision Trees

My visual broke after upgrade : where to look ?

What changed visually ?
├── Borders disappear or take wrong color
│       → default border-color is currentColor in v4. Add explicit
│         border-gray-200 on every bordered element, OR shim in
│         @layer base.
├── Focus rings are thin / invisible
│       → default ring-width dropped from 3px to 1px. Replace `ring`
│         with `ring-3`, OR shim.
├── Shadows look wrong (too soft / too sharp)
│       → shadow scale shifted. shadow-sm became shadow-xs. shadow
│         became shadow-sm.
├── Variant order broke (first / last / odd / even on children)
│       → stacking order flipped. Swap right-to-left to left-to-right.
├── bg-opacity / text-opacity ignored
│       → removed. Use the slash modifier on the color utility.
├── !important utilities ignored
│       → moved from leading to trailing bang.
├── Custom prefix classes do not parse
│       → prefix is now a variant, not a string concat.
└── Arbitrary [--var] values do not resolve
        → use ([--var]) parentheses syntax now.

Should I codemod or rewrite ?

Codebase size ?
├── < 50k LOC : run npx @tailwindcss/upgrade then audit manually
├── 50k-200k  : codemod + this trap catalog + visual regression suite
└── > 200k    : staged migration with @layer base shims for default-change traps,
                codemod per package, full audit per merge

Patterns

Pattern : variant stacking order (L-004)

v3 evaluated variants right-to-left :

<!-- v3 : "first child, of every direct child" -->
<ul class="first:*:pt-0">

v4 evaluates left-to-right :

<!-- v4 : "every direct child, when it is first" -->
<ul class="*:first:pt-0">

ALWAYS swap stacked variants when the new order changes semantics. The codemod catches the common forms but multi-token chains (group-hover:*:first:opacity-100) need manual review.

Pattern : default border-color shim (L-005)

To preserve v3 default border color :

@layer base {
  *, ::after, ::before, ::backdrop {
    border-color: var(--color-gray-200, currentColor);
  }
}

ALWAYS treat this as TEMPORARY. The right long-term fix is to add explicit border-gray-200 (or your token) to every bordered element. Leave the shim in for one release cycle, then audit and remove.

Pattern : default ring shim

@layer base {
  * {
    --tw-ring-color: var(--color-blue-500);
  }
}

Or replace every ring with ring-3 ring-blue-500 at the call site.

Pattern : shadow scale renames (L-006)

Apply these renames everywhere :

v3 v4
shadow-sm shadow-xs
shadow shadow-sm
drop-shadow-sm drop-shadow-xs
drop-shadow drop-shadow-sm
blur-sm blur-xs
blur blur-sm
backdrop-blur-sm backdrop-blur-xs
backdrop-blur backdrop-blur-sm
rounded-sm rounded-xs
rounded rounded-sm

The codemod handles these. ALWAYS visually diff a representative page after to confirm no edge case was missed.

Pattern : removed corePlugins / safelist / separator (L-003)

corePlugins removal :

- // tailwind.config.js (v3)
- corePlugins: { preflight: false }

v4 replacement : drop preflight by NOT importing the preflight layer. Use @layer base { ... } to write your own base styles instead.

safelist removal :

- // tailwind.config.js (v3)
- safelist: ["bg-red-500", "bg-green-500"]

v4 replacement (in entry CSS) :

@source inline "{bg-red-500,bg-green-500}";

separator removal : v4 only supports :. NEVER attempt to use __ or any other separator.

Pattern : opacity modifier

- <div class="bg-black bg-opacity-50">
+ <div class="bg-black/50">

Same applies to text-opacity, border-opacity, divide-opacity, ring-opacity, placeholder-opacity. All removed.

Pattern : prefix flip

- <div class="tw-flex tw-bg-red-500 tw-hover:bg-red-600">
+ <div class="tw:flex tw:bg-red-500 tw:hover:bg-red-600">

Set prefix in CSS (v4) :

@import "tailwindcss" prefix(tw);

Pattern : important modifier flip

- <div class="!flex !bg-red-500">
+ <div class="flex! bg-red-500!">

Inside @apply :

- .btn { @apply font-bold !important; }
+ .btn { @apply font-bold!; }

Pattern : outline-none rename

- <input class="focus:outline-none">
+ <input class="focus:outline-hidden">

The v4 outline-none now actually sets outline-style: none, which removes the focus outline even in forced-colors mode. ALWAYS prefer outline-hidden (the new name for v3 behavior) for accessibility.

Pattern : arbitrary CSS var syntax

- <div class="bg-[--brand-color]">
+ <div class="bg-(--brand-color)">

Parentheses replaced square brackets for CSS-var references because modern CSS parsing made square-bracket syntax ambiguous.

Pattern : underscore for spaces in arbitrary values

- <div class="grid-cols-[max-content,auto]">
+ <div class="grid-cols-[max-content_auto]">

Use _ for the space ; comma no longer becomes a space substitute.

Pattern : individual transform reset

- <button class="scale-150 focus:transform-none">
+ <button class="scale-150 focus:scale-none">

v4 splits transform into individual CSS properties (rotate, scale, translate). Reset each individually with *-none.

Pattern : @layer utilities to @utility

- @layer utilities {
-   .tab-4 { tab-size: 4; }
- }
+ @utility tab-4 {
+   tab-size: 4;
+ }

The new @utility directive supports --value(), --modifier(), --alpha(), --spacing() helpers for dynamic utilities. See tailwind-impl-config-v4.

Pattern : hover gating

v4 wraps hover: in @media (hover: hover) so it does NOT trigger on tap devices. ALWAYS pair stateful hover with an explicit active: / focus: variant for touch users :

<button class="bg-blue-500 hover:bg-blue-700 active:bg-blue-800">
  Tap-friendly
</button>

Anti-Patterns (summary)

NEVER trust the codemod's silence on default-behavior changes. Border color and ring width changes produce visual regressions, not build errors.

NEVER swap variant order mechanically without checking semantics. The flip from right-to-left to left-to-right can change meaning when multiple structural variants stack.

NEVER ship the @layer base default-color shim long term. It hides the behavior change from new contributors.

NEVER use bg-opacity-* / text-opacity-* in v4. They do nothing.

See references/anti-patterns.md for the full catalog.

Reference Links

  • references/methods.md : exact rename table, codemod command, config-option migration map, browser-support matrix
  • references/examples.md : before/after snippets for every trap, staged @layer base shims, audit scripts
  • references/anti-patterns.md : every wrong assumption during upgrade with symptom, root cause, fix

Sources