Tailwind CSS

Migrating to Tailwind v4 and its new Oxide engine

TW

Tailwind v4's rewritten Oxide engine, along with CSS-first configuration replacing the old JavaScript config file, is the biggest change to the framework since utility-first CSS itself. We migrated three active projects to see how smooth the transition actually is, deliberately choosing projects of different ages and different levels of customization so we'd get an honest read on how the migration experience varies rather than just testing on our newest, cleanest codebase and calling it representative.

The build performance story

Build times dropped dramatically across all three projects, most noticeably on the largest one, where full rebuilds had been a real point of friction under v3. The Oxide engine's rewrite in a lower-level language than the previous JavaScript-based implementation shows up most clearly on exactly the kind of large, utility-class-dense project where the old engine's overhead compounded the most. On our smallest project, a marketing site with a modest set of components, the build time improvement was real but not something anyone would have noticed without measuring it directly. On the largest, a client application with years of accumulated component styling, full rebuild time dropped enough that it changed how developers worked day to day, incremental rebuilds during active development stopped being a moment where attention drifted to a Slack notification while waiting.

We measured this carefully rather than relying on impression, since build performance claims around major version changes are exactly the kind of thing that's easy to overstate based on a good first impression. Across repeated builds on consistent hardware, the largest project's cold build time dropped by more than half, and incremental rebuild time during active development dropped by a similar margin. Those numbers matched what we'd read in Tailwind's own release material closely enough that we came away trusting the vendor's performance claims more than we typically default to trusting a major version's own marketing.

Living with CSS-first configuration

Moving configuration into CSS itself, rather than a JavaScript config file, took some real adjustment for the team, but ended up feeling more natural once we stopped reaching for the old mental model. The JavaScript config file had become such a fixture of how we thought about Tailwind that several engineers initially tried to recreate its exact structure inside the new CSS-based configuration, which works but misses most of what actually makes the new approach better. Once the team let go of that instinct and started configuring the way the new system actually wants to be configured, defining design tokens as CSS custom properties directly, the workflow started to click.

  • CSS-first configuration removes an entire category of JavaScript-to-CSS translation that the old config format required implicitly.
  • Design tokens defined as native CSS custom properties are inspectable directly in browser developer tools, which the old JavaScript config never allowed.
  • Editor tooling and autocomplete for the new configuration format lagged behind the JavaScript config's tooling maturity for the first few months.
  • Teams with heavily customized v3 configs should budget real time for manually translating plugin-dependent configuration rather than expecting an automated path.

One genuine improvement we hadn't fully appreciated until living with it: design tokens defined as native CSS custom properties are directly inspectable in browser developer tools in a way the old JavaScript configuration never allowed. A developer debugging a spacing or color issue can now inspect the actual custom property value live in the browser rather than cross-referencing a separate configuration file to understand where a utility class's underlying value came from. That's a small thing in any single instance and a real quality-of-life improvement in aggregate across a team debugging styling issues all day.

The one project that wasn't smooth

The migration was smoother than we expected for two of the three projects. The third, with a heavily customized v3 config and several plugins, needed real manual work, which is the honest caveat for anyone with a similarly customized setup. That project had accumulated several third-party plugins over its lifetime, each extending Tailwind's utility generation in project-specific ways, and v4's plugin API changes meant none of them migrated automatically. We ended up rewriting the functionality of two of those plugins directly as CSS-first configuration, which the new system's flexibility made possible, but which still meant real engineering time spent translating logic that used to live in a JavaScript plugin function into the new declarative format.

The automated migration tooling Tailwind provides handled the mechanical parts of this project's transition well, converting the bulk of the JavaScript config into its CSS-first equivalent without issue. What it couldn't do was decide how to handle the custom plugin logic, since that logic represented actual product decisions somebody had made in JavaScript years earlier, decisions the migration tool had no way to infer the intent behind. That's the right limitation for an automated tool to have, but it means teams with a similarly customized setup should budget real calendar time for this project specifically, rather than assuming the automated tooling will carry them the whole way.

What we'd tell another team planning this migration

Start with your simplest, least customized project if you're doing this incrementally, both to build team familiarity with the new configuration model and to get a genuine, uncomplicated read on the build performance improvements before you hit a project complex enough to obscure them. Budget disproportionate time for any project carrying custom plugins or a heavily bespoke v3 configuration, since that's reliably where the real migration cost concentrates, not in the mechanical parts of the transition. And measure your own build times before and after rather than relying on general impressions, the improvement is real enough that it's worth having concrete numbers to justify the migration time to anyone asking whether it was worth it.

A rewrite of this scale carries real risk of subtle visual regressions that don't show up as an obvious build failure, a slightly different computed value for some utility class edge case that renders correctly in the majority of cases but drifts in an uncommon combination nobody thought to check by eye. Given how much of our client work depends on pixel-level visual consistency, we didn't feel comfortable relying on manual spot-checking alone across three production codebases with years of accumulated component variety between them.

We ran automated visual regression testing across a broad set of component states and page templates on each project, comparing rendered output before and after the migration pixel by pixel rather than just checking that the build succeeded and looked roughly right on a quick pass. That process caught a small number of genuine differences, mostly around how certain arbitrary-value utility classes computed under the new engine compared to the old one, differences small enough that they might easily have gone unnoticed in casual review but real enough that we wanted to resolve them deliberately rather than ship them as an unintended side effect of the migration.

Beyond the mechanical configuration changes, we ran an internal session specifically on how to think about styling under the new CSS-first approach, since we'd noticed engineers defaulting to old patterns out of habit rather than because those patterns were still the best fit under v4. That session focused less on syntax, which most engineers picked up quickly from the migration guide, and more on the underlying philosophy shift: treating design tokens as first-class CSS values that other CSS and JavaScript can both reference directly, rather than as configuration that only Tailwind's build step understood.

That reframing mattered more than we expected for a few engineers who'd built years of intuition around the old JavaScript configuration model and needed a genuine mental shift, not just a syntax update, to use the new system well rather than merely functionally. We've since made that session a standard part of onboarding any engineer joining a v4 project, treating the philosophy shift as something worth teaching explicitly rather than assuming it would be picked up implicitly from reading the migration guide alone.

Given how much of our client work is visually sensitive, we went into every migration with an explicit rollback plan rather than assuming the transition would succeed cleanly on the first attempt. That meant keeping the pre-migration branch deployable up to the moment we were confident in the visual regression results, and staging the migration behind a feature flag on the two projects where a staged, gradual rollout was practical rather than an all-at-once cutover. In the end we never actually needed to exercise the rollback plan on any of the three projects, but having it in place changed how the team approached the migration, engineers were willing to move faster and be more decisive about resolving edge cases during the transition precisely because a mistake wasn't going to mean an emergency all-hands revert.

That's a pattern we've noticed holds beyond this specific migration: the presence of a credible rollback path tends to make a team move through a risky change faster and more confidently, not just safer, since the psychological weight of "this has to work the first time" is itself a drag on decision-making speed during a migration. We've started building that same rollback discipline into how we scope other major framework upgrades going forward, treating a tested rollback plan as a standard deliverable rather than an optional extra reserved for the highest-risk projects only.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients