Capítulo 14 de 29

Chapter 14: Upgrade Guide (v3 → v4)

Core Idea

Tailwind v4 is a new engine (Lightning CSS-based) with a CSS-first configuration model — most v3→v4 migration is handled by npx @tailwindcss/upgrade (Node 20+, run on a branch, review the diff), but there's a long list of behavioral/naming changes worth knowing even when the tool does the mechanical work.

Key Concepts

  • Upgrade tool: npx @tailwindcss/upgrade migrates dependencies, converts tailwind.config.js to CSS, and updates template files automatically for most projects — run it on a branch and review the diff rather than trusting it blindly on a complex project.
  • Package changes: the PostCSS plugin moved to @tailwindcss/postcss (drop postcss-import/autoprefixer, both now automatic); Vite users should switch to the dedicated @tailwindcss/vite plugin; the CLI moved to @tailwindcss/cli.
  • @import "tailwindcss" replaces @tailwind base/components/utilities — a single regular CSS import instead of three custom directives.
  • Browser floor unchanged from v4.0's baseline: Safari 16.4+, Chrome 111+, Firefox 128+ (same as covered in the Compatibility chapter) — v4 depends on @property and color-mix(), so older browsers need to stay on v3.4.

Renamed / removed utilities

Deprecated (removed)Replacement
bg-opacity-*, text-opacity-*, border-opacity-*, divide-opacity-*, ring-opacity-*, placeholder-opacity-*Opacity modifier, e.g. bg-black/50
flex-shrink-*shrink-*
flex-grow-*grow-*
overflow-ellipsistext-ellipsis
decoration-slice / decoration-clonebox-decoration-slice / box-decoration-clone
v3v4Why
shadow-smshadowshadow-xsshadow-smShadow/radius/blur scales renamed so every step has an explicit name; bare shadow/rounded/blur still work but now render like the old -sm step
drop-shadow-smdrop-shadowdrop-shadow-xsdrop-shadow-smSame renumbering
blur-smblurblur-xsblur-smSame renumbering
backdrop-blur-smbackdrop-blurbackdrop-blur-xsbackdrop-blur-smSame renumbering
rounded-smroundedrounded-xsrounded-smSame renumbering
outline-noneoutline-hiddenoutline-none in v3 didn't actually set outline-style: none (kept an invisible outline for forced-colors accessibility) — v4's outline-hidden preserves that behavior, and a new outline-none now genuinely sets outline-style: none
outline outline-2outline-2 (alone)outline-<n> now implies outline-style: solid; outline alone now also sets outline-width: 1px
ring (3px)ring-3Default ring width dropped from 3px to 1px for consistency with border/outline; also default ring color changed from blue-500 to currentColor

Selector / behavior changes

  • space-x-*/space-y-* and divide-x-*/divide-y-* switched from a :not([hidden]) ~ :not([hidden]) sibling selector to :not(:last-child) for performance on large pages — can shift behavior with inline elements or hand-tuned child margins; migrating to flex/grid + gap-* avoids the whole selector question.
  • Gradient variants no longer reset the whole gradient: overriding from-* in a variant (e.g. dark:from-blue-500) used to blow away to-*/via-* too; in v4 the other stops persist, so explicitly use via-none if you need to collapse a three-stop gradient back to two stops in a specific state.
  • container utility lost its config options (center, padding) — customize it via @utility container { margin-inline: auto; padding-inline: 2rem; } instead.
  • Default border-*/divide-* color changed from gray-200 to currentColor — always specify a color explicitly now, or restore old behavior with a @layer base rule setting border-color: var(--color-gray-200, currentColor).
  • Preflight: placeholder color now defaults to current text color at 50% opacity (was gray-400); buttons default to cursor: default (was pointer); <dialog> margins are reset (add margin: auto in @layer base to restore centering); the hidden attribute now always wins over display utilities like block/flex.
  • Prefixes (prefix(tw)) now behave like a variant at the very start of the class (tw:flex, tw:hover:bg-red-600) instead of a config-driven class-name prefix; @theme is still written unprefixed, but generated CSS variables are prefixed (--tw-color-...).
  • !important modifier moves to the end of the class (bg-red-500!, hover:bg-red-600/50!) instead of the start — old leading ! still works but is deprecated.
  • Custom utilities: @layer utilities/@layer components class hijacking is gone — use @utility instead; custom utilities now sort by property count, so a .btn-style utility can be overridden by more specific Tailwind utilities without extra config.
  • Variant stacking order flipped: v3 applied stacked variants right-to-left; v4 applies left-to-right (matches CSS reading order) — reverse any order-sensitive stacks (e.g. first:*:pt-0*:first:pt-0).
  • Arbitrary CSS-variable shorthand changed from square brackets to parentheses: bg-[--brand-color]bg-(--brand-color) (ambiguity with newer CSS syntax forced the change).
  • Commas in arbitrary values (grid-cols-*, grid-rows-*, object-*) are no longer auto-converted to spaces — use underscores explicitly (grid-cols-[max-content_auto], not [max-content,auto]).
  • hover: now respects (hover: hover), so it no longer fires on tap for touch-only devices by default — restore old always-fires behavior with @custom-variant hover (&:hover); if a project depends on it (not recommended long-term).
  • transition/transition-colors now include outline-color — an outline color set only inside a state variant will visibly transition from the default; set the color unconditionally instead.
  • rotate-*/scale-*/translate-* are now individual CSS properties, not part of a combined transform value: transform-none no longer resets them (reset the individual utility instead, e.g. scale-none), and a custom transition-[...] list must name the individual properties (transition-[opacity,scale], not transition-[opacity,transform]) to keep them animating.
  • corePlugins config option is gone — no more disabling whole utility families via config.
  • theme() function: prefer the generated var(--token) CSS variables; where theme() is still required (e.g. inside a media query, which can't take a CSS variable), use the CSS variable name instead of v3's dot notation (theme(--breakpoint-xl), not theme(screens.xl)).
  • JS config files still work via explicit @config "./tailwind.config.js"; but are no longer auto-detected; corePlugins/safelist/separator config options aren't supported at all in v4 (use @source inline() for safelisting).
  • resolveConfig (JS) removed — read theme values as CSS variables (var(--color-blue-500)) or via getComputedStyle instead of a flattened JS config object.
  • Scoped stylesheets (CSS Modules, Vue/Svelte/Astro <style>) no longer implicitly see theme/custom-utility/custom-variant definitions from other files — add @reference "<path>"; before using @apply, or switch to var(--token) (also faster, since Tailwind skips processing that file).

Key Takeaways

  1. Always run the upgrade tool on a branch first, then read the diff — the mechanical renames (shadow/blur/radius scale, ringring-3, outline-noneoutline-hidden) are exactly the kind of change that's easy to miss by eye.
  2. The -sm-labeled utilities across shadow/blur/radius/backdrop-blur all quietly shrank a step in v4 — search for these four families specifically even after running the tool.
  3. Selector-level changes (space-between, divide, gradient-variant reset behavior) can silently change rendered output without a build error — visually re-check large pages after upgrading.
  4. If depending on theme(), resolveConfig, corePlugins, or hover-always-fires-on-tap: these are the explicit "you need a decision" migration points, not automatic.

Connects To

  • Theme: @theme/CSS variables are the v4 replacement for tailwind.config.js and the theme() function.
  • Functions and Directives: @reference, @config, @utility, important import flag — the v4-native mechanisms several of these migrations move toward.
  • Compatibility: browser floor and CSS Modules/preprocessor guidance shared with this chapter.