SN.
All Articles
Front-EndAugust 7, 20268 min read

Upgrading to Tailwind CSS v4 Without Regressing the UI

I treat Tailwind v4 as an infrastructure migration, not a redesign: establish a visual baseline, map tokens, update the build, then verify responsive, RTL, and interaction states.

Close-up of CSS source code on a dark monitor

An infrastructure upgrade is successful when the user does not discover an accidental redesign at the end of it.

Tailwind CSS v4 is a useful case study. The change is bigger than replacing a few utility classes; configuration, theme variables, and build integration have evolved. If typography, spacing, and component styling are “improved” at the same time, the first regression becomes difficult to trace.

I separate the upgrade from redesign work and establish a visual contract before touching the dependency.

I baseline the product before I baseline the package

The first task is defining what must remain stable. For a production front end, that usually includes:

  • primary colors and design tokens;
  • font families, weights, line heights, and font-loading behavior;
  • container widths and breakpoints;
  • grid and stacking behavior;
  • important borders, radii, and shadows;
  • hover, focus-visible, disabled, and active states;
  • RTL/LTR behavior;
  • sensitive screens such as navigation, checkout, dashboards, or modals;
  • existing build warnings and production output.

If the project has no automated visual-regression setup, I still capture reference screenshots for important viewports and flows. The goal is not a perfect pixel-diff system. It is to stop relying on memory as the specification.

Tailwind v4 forces a useful token conversation

Tailwind v4 has a more CSS-first model, and theme variables can be defined with @theme so they participate in both CSS variables and generated utilities.

@import "tailwindcss";

@theme {
  --color-accent: #ff6a00;
  --breakpoint-3xl: 120rem;
}

The syntax is not the interesting part of a migration. The better question is: where do the project’s design tokens actually live today?

A mature codebase may have the brand color in configuration, a CSS variable, several arbitrary values, and hard-coded component classes under different names. An upgrade is a good time to make that source of truth explicit without changing the design itself.

I map old tokens to new tokens first. Only after consumers are stable would I consider changing the token values as a separate design task.

I migrate in slices, not with one large replacement

A typical sequence for me looks like this:

  1. Bring up the new build integration.
  2. Move base CSS and fonts while preserving appearance.
  3. Map the required tokens and custom utilities.
  4. Fix components affected by changed build or utility behavior.
  5. Compare responsive and interaction states with the baseline.
  6. Consider non-essential cleanup only after parity is reached.

This can look slower than a large search-and-replace. In practice, it makes the first regression dramatically easier to isolate.

Typography is where “small” regressions become visible

A font is more than font-family. Changes in build order, variable-font loading, fallback behavior, or base styles can make a heading wrap differently even when no component class was edited.

That matters even more on bilingual products. A type scale that works for an English display face may be wrong for a Persian UI font. Tight line heights that look intentional in Latin text can clip or compress Persian headings.

During an infrastructure upgrade I verify that:

  • the same font assets are still loading;
  • required weights exist;
  • line height and letter spacing have not been reset accidentally;
  • long headings wrap correctly around boundary widths;
  • fallback-to-webfont swaps do not create unexpected layout movement.

If typography needs redesign, I track that separately. I do not hide it inside the dependency migration.

A breakpoint is a product behavior, not just a config value

Changing a breakpoint can alter a screen at 1024px or 1280px even when wide desktop and narrow mobile both look fine.

I intentionally test just before and after important breakpoints. Many layout bugs live in that boundary: the grid still uses its desktop columns, but the children no longer have enough real width.

I look for:

  • min-width rules that cause horizontal scrolling;
  • grid columns that cannot shrink around long content;
  • hidden/flex switches activating at the wrong point;
  • images picking up a different aspect ratio or crop;
  • sticky elements colliding with a header.

Responsive QA is not one 1440px screenshot and one 390px screenshot. The transitions between them are part of the component contract.

Hover and focus belong in the baseline too

A regression is not always visible in a static screenshot. A changed variant or utility can remove hover, focus-visible, disabled, or group states while the default component looks correct.

For interactive components I explicitly check:

  • keyboard focus on links and buttons;
  • focus-ring visibility and contrast;
  • hover behavior where hover is available;
  • disabled states that remain readable;
  • group/peer interactions;
  • transitions still targeting the intended properties.

Accessibility does not become optional because the task is “just a CSS upgrade.”

RTL exposes physical assumptions quickly

Setting dir="rtl" is not enough for a bilingual product. Refactors can surface old assumptions encoded with left, right, ml, or mr.

During migration I scan for cases such as:

  • physical margin utilities where logical ms/me is more appropriate;
  • badges or decorative elements pinned to one physical side;
  • arrows that need locale-aware direction;
  • gradients whose direction depends on image placement;
  • LTR technical text embedded in an RTL sentence.

The goal is not to redesign RTL during the Tailwind migration. It is to make sure the new infrastructure preserves the already-approved behavior.

I audit build configuration separately from visual changes

Tailwind v4 integrates more directly with modern CSS and build tooling, but legacy projects have their own assumptions. I do not delete an old config file or plugin until I know what still depends on it.

The audit includes:

  • current PostCSS or Vite integration;
  • Tailwind plugins and custom utilities;
  • source detection and dynamically composed class names;
  • old CSS preprocessors;
  • import order;
  • minification and production output;
  • the browser-support requirement of the product.

Tailwind v4 relies on modern CSS features and has stricter browser requirements than older releases. If the product must support older browsers, “latest version” is not enough justification for the upgrade. Compatibility is a product requirement, not a tooling preference.

Dynamic classes deserve a production-build check

Utility-based systems can fail in a subtle way when class names are assembled dynamically and source detection cannot see the final strings.

Instead of open-ended string construction, I prefer an explicit mapping when variants are finite:

const toneClass = {
  danger: "text-red-500 border-red-500/30",
  success: "text-emerald-500 border-emerald-500/30",
};

This is not unique to Tailwind v4. An infrastructure upgrade is simply a good moment to uncover build assumptions that happened to work before.

My post-migration regression checklist

Visual

  • Colors and tokens match the baseline.
  • Typography and wrapping are stable in every supported language.
  • Borders, radii, shadows, and opacity are unchanged.
  • Image crops and aspect ratios still match the design.

Responsive

  • Mobile, tablet, and desktop layouts work.
  • Widths around breakpoints do not introduce horizontal scrolling.
  • Grid and navigation are checked at transition points.
  • RTL/LTR behavior remains correct in direction-sensitive layouts.

Interaction

  • Hover, focus-visible, active, and disabled states still work.
  • Animation and transitions have not disappeared.
  • Keyboard navigation is intact.

Build

  • The production build succeeds.
  • There are no unexplained new warnings.
  • Asset and font paths resolve correctly.
  • A still-required legacy dependency was not removed by accident.
  • The browser target matches the product requirement.

When I postpone the upgrade

If a project has mandatory older-browser support, a sensitive release is close, regression coverage is weak, or the team has no reliable visual baseline, delaying the upgrade can be the more professional decision.

Engineering is not a race to run the newest version first. It is the work of reducing risk while keeping future changes understandable.

The wider lesson

For me, Tailwind CSS v4 is less interesting as a syntax update than as a case study in changing infrastructure without changing the product.

Baseline first, map the existing system, migrate in controlled slices, then clean up. That order lets the build and token model move forward without making the approved interface pay the cost of experimenting with the tooling.