Tailwind CSS a Year Later: Where It Helps, Where It Hurts

By James Nguyen Updated September 24, 2026
Tailwind CSS a Year Later: Where It Helps, Where It Hurts

A year ago I migrated our team's main product from a hand-rolled CSS module setup to Tailwind, skeptical of utility classes cluttering markup, and rode out the v4 upgrade with its CSS-first configuration partway through that year. Enough time has passed to have a real opinion beyond the initial "this feels weird" reaction utility-first CSS gives almost everyone at first.

The v4 Config Change Was Bigger Than It Looked

Moving from a JavaScript tailwind.config.js to defining design tokens directly in CSS with the @theme directive, which is the headline change in v4, felt cosmetic when I first read the migration guide and turned out to matter more in practice. Design tokens now exist as real CSS custom properties usable outside Tailwind's own utility generation, which let us wire a couple of tokens directly into a JavaScript animation library without exporting a parallel copy of the same values.

@import "tailwindcss";
@theme {
  --color-brand: oklch(0.7 0.15 250);
  --spacing-section: 6rem;
}

Build Speed Stopped Being Something I Think About

The Oxide engine, Tailwind's Rust-based rewrite of the build pipeline that shipped with v4, changed our full build time from a noticeable pause to something closer to instant, and incremental rebuilds during development are fast enough that I genuinely stopped noticing them as a step in the loop at all. This sounds like a minor quality-of-life detail until you remember how many times a day a full-time frontend developer triggers a rebuild.

Where Utility Classes Genuinely Help

The actual case for Tailwind, a year in, isn't really about writing CSS faster, it's about deleting CSS. We don't have an accumulating stylesheet of one-off classes with vague names that nobody's sure is still used anywhere, because the styling lives directly at the point of use and dead markup takes its styling with it when it's deleted. That alone eliminated a category of "is this CSS class still referenced anywhere" archaeology that used to eat real time during cleanup work.

Where It Genuinely Hurts: Markup Readability

The honest downside is markup density. A component with a dozen conditional utility classes strung together in a single className string is objectively harder to scan than a single semantic class name, and new team members consistently need longer to parse a complex component's JSX for this reason before they build up pattern recognition for common utility combinations. Extracting truly repeated patterns into a component rather than copy-pasting the same utility string everywhere helps, but doesn't eliminate this cost.

Dark Mode Got Genuinely Harder With v4's Design

Defining colors with @theme inline bakes those values in at build time, which quietly broke runtime dark mode toggling the way we'd originally set it up. The fix, keeping raw color channel values in :root and a .dark selector, then mapping them through a non-inline @theme reference, works, but it's a two-stage setup that wasn't obvious from the migration guide and cost us a confused afternoon the first time dark mode silently stopped switching after the v4 upgrade.

Component Library Compatibility During the Upgrade

The official upgrade tool handled the bulk of our v3-to-v4 migration automatically, but a couple of third-party component libraries we depend on hadn't updated their internal Tailwind usage yet at the time we upgraded, which meant patching around version mismatches manually for a few weeks until upstream caught up. Anyone managing several projects with different component library dependencies should expect this lag rather than assuming a same-day clean upgrade everywhere.

Would I Choose It Again for a New Project

Yes, without much hesitation, but I'd set the expectation with new team members upfront that the markup-density tradeoff is real and worth being deliberate about, extracting components for genuinely repeated patterns rather than letting every element carry its own sprawling utility string. That single habit, established early, prevents most of the readability complaints I hear from teams new to the framework.

Container Queries Solved a Problem We Used to Fight With JavaScript

Before v4 made container queries a first-class utility, sizing a card component based on the width of its container rather than the viewport meant reaching for a JavaScript ResizeObserver and manually toggling classes. Being able to write @container and size-based utility variants directly in CSS removed a whole small library of that JavaScript glue code from our component layer, and it's the kind of change that's easy to undersell in a changelog bullet point but genuinely simplified real components we ship.

The Design Handoff Improved More Than I Expected

Our designer working in Figma had always struggled to hand off exact spacing and color values that mapped cleanly onto our old, more ad-hoc CSS variable naming. Standardizing on Tailwind's utility scale as the shared vocabulary, since it's the same scale visible in both the design tool's plugin and our actual @theme tokens, cut down the back-and-forth over "is this really 16px or 18px" that used to eat time in design review meetings.

Onboarding Backend-Leaning Engineers Onto Utility Classes

A couple of engineers on our team who work primarily on backend services but occasionally touch the frontend found the utility class approach genuinely faster to pick up than expected, since they weren't unlearning years of hand-written CSS habits the way our dedicated frontend developers occasionally had to. That was a small, unplanned benefit of the migration, lowering the barrier for occasional frontend contributors rather than raising it.

Final Verdict

A year and a major version upgrade later, Tailwind's core value proposition, styling that gets deleted along with the markup it belongs to, held up better than I expected going in skeptical. The v4 rewrite's build speed and CSS-first tokens are genuine improvements, not just version-number theater, but the dark mode gotcha and markup readability tradeoff are real costs worth planning for rather than discovering mid-project.

Daniel Justin

About the Author

James Nguyen is a full-stack programmer with more than ten years of experience engineering software systems. Specializing in the Node.js and Python ecosystems, he focuses on backend architecture, API design, and clean data integration. Follow me on YouTube and Instagram.

More Articles