class="flex items-center justify-between p-4 bg-white rounded-lg shadow-md" class="text-lg font-semibold text-gray-900 hover:text-blue-600 transition-colors" class="mt-2 text-sm text-gray-500 dark:text-gray-400 line-clamp-2" The CSS that reads like a blueprint Dev Tools

Tailwind CSS Review 2025: I Hated It, Then I Shipped Faster Than Ever

AM
Arjun Mehta
February 10, 2025
15 min read

A Brief History of Fighting About CSS

Before we talk about Tailwind, we need to talk about how we got here, because this framework only makes sense in the context of the war it was born from.

In the beginning, there was plain CSS, and it was chaos. Stylesheets grew into thousand-line monstrosities where changing a single color meant grep-searching through a file and hoping you did not break something three pages away. Then came the naming conventions -- BEM, OOCSS, SMACSS -- each a noble attempt to impose order through discipline. Write your classes with double-underscores and double-dashes and everything will be maintainable. Except nobody on the team could remember whether it was .card__header--active or .card-header__active, and the styles still leaked in unexpected ways.

CSS-in-JS arrived as the React era's answer: just write your styles in JavaScript, co-located with your components. Styled Components, Emotion, CSS Modules. These solved the scoping problem beautifully. No more global namespace pollution. No more naming debates. But the runtime cost was real -- generating styles at execution time added to bundle size and slowed down initial renders. And debugging meant digging through auto-generated class names like .sc-bdVTJa.eiVXPq, which gave absolutely no indication of what the element was supposed to be.

Then in 2017, Adam Wathan released Tailwind CSS, and the internet lost its collective mind.

The pitch was simple: instead of writing custom CSS classes, you compose designs from a fixed set of small, single-purpose utility classes directly in your HTML. Want a flexbox container with padding, a white background, rounded corners, and a shadow? Write class="flex p-4 bg-white rounded-lg shadow". No separate stylesheet. No naming. No context-switching between HTML and CSS files. Just look at the markup and you can see exactly what it looks like.

The criticism was immediate and visceral. "This is just inline styles with extra steps." "The HTML is unreadable." "Separation of concerns is dead." I was in that camp. I looked at Tailwind examples and felt a physical revulsion at the long strings of classes cluttering otherwise clean markup. It violated everything I had been taught about writing maintainable frontend code. I was wrong.

The Refactoring That Changed My Mind

The turning point came six months ago when I was maintaining a medium-sized dashboard application. It had been built with BEM-style CSS, and after two years and four different developers, the stylesheet was 4,200 lines long. I needed to add a new card component that looked similar to an existing one but with different spacing and a darker header. Simple task. Should take an hour.

Three hours later, I was still untangling which styles were shared, which were overridden, which had specificity conflicts from a previous developer's !important sprinkled through the code. I changed a margin on the card, and it broke the layout of a completely different page because both shared a base class that had been modified six months ago for a reason no one documented.

That night, I converted a single page of the dashboard to Tailwind as an experiment. Not the whole app. Just one page. And something clicked.

The card component that had taken three hours to modify in BEM took about 20 minutes to rebuild in Tailwind. Not because Tailwind is naturally faster to type -- it is not -- but because every style decision was visible in the component markup. There was no hidden cascade. No specificity wars. No hunting through a 4,200-line stylesheet to find which rule was creating the unexpected gap. When I looked at the component, I could see p-6 bg-gray-800 rounded-xl shadow-lg and know exactly what it looked like without opening a single CSS file.

Over the following month, I migrated the entire dashboard. The 4,200-line CSS file disappeared. What replaced it was about 30 lines of Tailwind configuration (custom colors, font sizes, and breakpoints to match the existing design system) and a tiny 80-line CSS file for the handful of things that genuinely needed custom styles -- complex animations, a custom scrollbar, and a third-party library override. The total CSS shipped to the browser dropped by 60% because Tailwind's purge system only includes the utility classes you actually use.

Building with Tailwind v4: What Changed

Tailwind v4, released in early 2025, is the most significant update to the framework since its inception. The headline change is that the PostCSS plugin and the JavaScript-based configuration file are gone. Tailwind v4 is built on a new Rust-based engine called Oxide that is dramatically faster -- full builds in the single-digit milliseconds instead of the hundreds of milliseconds range. Configuration now lives in CSS itself using the @theme directive, which means your design tokens are defined in the same language they are consumed in.

Tailwind v3 tailwind.config.js PostCSS plugin ~350ms build Tailwind v4 @theme in CSS Oxide (Rust engine) ~5ms build

The other major v4 improvement is automatic content detection. In v3, you had to configure which files Tailwind should scan for class names, and getting that configuration wrong meant either missing classes in production or including too many. V4 figures it out automatically based on your project structure. It just works. I have not thought about content configuration once since migrating.

Container queries arrived with v4 as well, and they solve a long-standing problem with responsive utility classes. Traditional responsive prefixes like md:grid-cols-3 respond to the viewport width, but components often need to adapt based on their own container width. With container queries, you can write @md:grid-cols-3 and the layout adapts to the parent container's width instead. This makes truly reusable responsive components possible in a way that was clunky before.

The Daily Experience of Writing Tailwind

Let me tell you what an average morning of building looks like with Tailwind, because the feel matters more than the feature list.

I open a React component file. I need a card with a user avatar on the left, name and role stacked on the right, and a status badge in the top-right corner. In BEM-era development, I would create the HTML structure, think of semantic class names (.user-card, .user-card__avatar, .user-card__info, .user-card__badge), switch to the CSS file, write the styles, switch back, check the browser, adjust, switch files again. The context-switching was constant.

With Tailwind, I stay in one file. The outer div gets flex items-center gap-4 p-4 bg-white rounded-xl shadow-sm. The avatar gets w-12 h-12 rounded-full object-cover. The name gets text-sm font-semibold text-gray-900. The badge gets absolute top-2 right-2 px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-700. I see the styles as I write the structure. The VS Code IntelliSense extension autocompletes class names and shows me the CSS each utility generates. I rarely leave the component file.

The dark mode support deserves particular mention because it is so effortless. Every utility can be prefixed with dark: to apply only in dark mode. bg-white dark:bg-gray-900. text-gray-900 dark:text-gray-100. Adding dark mode to a page that was designed for light mode is not a separate styling pass -- it is adding prefixed variants to the classes already there. I added dark mode to the entire dashboard in about two hours, and it worked correctly on the first try. That would have been days of work with traditional CSS.

The Things That Still Bother Me

I adopted Tailwind. I ship with it daily. And I still have genuine complaints.

The class strings get long. There is no way around it. A responsive, interactive element with dark mode support and hover states can easily have 20+ utility classes on a single HTML element. flex items-center gap-3 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg shadow-sm hover:bg-gray-50 hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-700 transition-all. That is a button. A single button. Reading that string and understanding the full visual output requires familiarity with the utility names that takes weeks to build. For new team members who have not internalized the vocabulary, it looks like random noise.

Component extraction helps -- in a React project, you build a Button component once and reuse it everywhere, so the long class string only exists in one place. But not every project is component-based. Server-rendered templates, email HTML, static pages -- these are places where you end up with long class strings repeated across multiple files, and that is genuinely harder to maintain.

I also miss the cascade sometimes, and I know that is a controversial thing to say. The cascade, for all its problems, was powerful for theming. Changing a CSS custom property at the root level and watching it propagate through every component was elegant in a way that search-and-replacing utility classes is not. Tailwind v4's @theme directive helps, and CSS custom properties work fine alongside Tailwind, but the framework's design philosophy pushes you away from cascading inheritance, and there are situations where that inheritance is exactly what you want.

The biggest philosophical problem is harder to articulate. Tailwind encodes design decisions in a fixed scale: p-1 is 4px, p-2 is 8px, p-3 is 12px, p-4 is 16px. These scales are well-chosen and produce aesthetically consistent results. But they are also a constraint. When a designer hands you a mock with 18px of padding -- not 16, not 20, but 18 -- you can use the arbitrary value syntax p-[18px], and it works, but it breaks the system. It is a utility that generates custom CSS rather than composing from the predefined scale. Overuse of arbitrary values erodes the consistency that makes Tailwind's design system valuable in the first place. In practice, I use arbitrary values about 5% of the time and feel slightly guilty each time.

Tailwind in the Broader Ecosystem

Tailwind's dominance in the utility-first space is undeniable, but alternatives exist and some are worth knowing about.

UnoCSS is the most interesting competitor. It is an atomic CSS engine that can be configured to understand Tailwind's class names but also supports entirely different presets. It is faster than Tailwind v3 was (though v4's Oxide engine has closed the gap) and more flexible in its configuration. For projects that want Tailwind's workflow but with more architectural control, UnoCSS is a legitimate choice. But its community and ecosystem are much smaller, which means fewer tutorials, fewer component libraries, and fewer Stack Overflow answers when you get stuck.

CSS Modules remain popular, especially in Next.js projects. They solve the scoping problem without utility classes, giving you real CSS with guaranteed isolation. The developer experience is different -- you are still writing traditional CSS -- but the safety of scoped styles with the ability to use the full power of CSS (including complex selectors, @keyframes, and cascade) appeals to developers who find Tailwind's approach too constraining.

Plain CSS with modern features -- nesting, container queries, the :has() selector, cascade layers -- has also gotten much more capable in 2025. The gap between "what you need a framework for" and "what native CSS can do" has narrowed considerably. For simpler projects, I sometimes question whether Tailwind is adding complexity that modern CSS does not require. But the moment the project grows past a certain size, the consistency and speed benefits of Tailwind reassert themselves.

Pricing: Open Source, But the Ecosystem Is Not

Tailwind CSS itself is completely free and open source under the MIT license. You can use it in any project -- personal, commercial, client work -- without paying anything. The core framework, the documentation, the VS Code extension, and the CLI are all free.

Tailwind UI is where the money comes from. It is a commercial library of professionally designed, production-ready component examples and page templates built with Tailwind. Individual packages start at $149 (marketing site components) and go up to $299 for the complete set of application UI, marketing, and ecommerce components. It is a one-time purchase with free updates.

Headless UI is a free companion library providing unstyled, accessible UI components (dropdowns, modals, tabs, comboboxes) that work perfectly with Tailwind's utility classes. Heroicons offers a free set of 300+ icons. These are genuinely useful and cost nothing.

For teams, Tailwind UI is a strong investment. The components are well-built, responsive, accessible, and serve as both usable code and educational examples. I have learned more about Tailwind's capabilities from reading Tailwind UI source code than from any tutorial. Whether you need it depends on your team's design resources -- if you have dedicated designers producing custom mockups, you may not use the components directly, but the patterns they demonstrate are valuable regardless.

Pros and Cons

Pros

  • Eliminates the constant context-switching between HTML and CSS files
  • No specificity conflicts, no cascade surprises, no naming debates
  • Tailwind v4's Rust-based Oxide engine compiles in single-digit milliseconds
  • Dark mode, responsive design, and hover states are just prefix variants away
  • Final CSS bundle contains only the utilities you use -- typically under 10KB compressed
  • Design system consistency is built into the constraint of the utility scale
  • VS Code extension with IntelliSense makes the class vocabulary learnable

Cons

  • Long class strings on complex elements are genuinely hard to read for the uninitiated
  • Non-component architectures suffer from class string duplication
  • Arbitrary values break the design system's consistency when overused
  • The learning curve for the utility vocabulary takes weeks before it feels natural
  • Some CSS patterns -- complex animations, cascading themes -- are awkward in utility-first
  • Team onboarding requires buy-in; Tailwind is polarizing and some developers resist it strongly

What I Still Do Not Like

The Verdict: 4.6 / 5

I want to be honest about the 0.4 I withheld, because I think it matters.

I do not like that Tailwind has made an entire generation of developers comfortable never learning how CSS actually works. The utility classes are an abstraction, and when the abstraction fails -- and it does, in edge cases involving container sizing, grid behavior, or z-index stacking contexts -- you need to understand the CSS underneath to debug it. Tailwind makes CSS faster but not simpler. The complexity is still there. It is just hidden behind a class name.

I do not like that HTML readability has been sacrificed for styling speed. The trade-off is worth it, in my experience, but it is a trade-off, and Tailwind advocates sometimes refuse to acknowledge it.

I do not like that the v3 to v4 migration, while well-documented, still required meaningful refactoring for projects using custom plugins or the JavaScript configuration file. Breaking changes are part of major versions, but the Tailwind ecosystem is large enough that the ripple effect was significant.

And yet. I build everything with Tailwind now. I am faster. My CSS is smaller. My components are self-documenting. My dark mode works. My responsive layouts come together in a fraction of the time they used to. The 4.6 is not given reluctantly. It is given honestly -- by someone who started as a skeptic, became a convert, and retained just enough skepticism to keep the rating grounded. Tailwind is the best way to style web applications in 2025. It is not the only way, and it is not without cost, and you should learn it anyway.

Comments (3)