Tailwind CSS adds more complexity than it removes

f13th1 pts0 comments

Tailwind CSS adds more complexity than it removes — Tina Mrak<br>CSS June 20, 2026<br>Tailwind CSS adds more complexity than it removes<br>Tailwind CSS adds more complexity than it removes

I keep going back and forth on Tailwind, and I usually end up somewhere in the middle. I understand the appeal. You move fast, you stop arguing about what to name a div, the styles sit right next to the markup, and once the workflow clicks it feels good to work in.

I’m not pretending any of that is made up.

What I can’t get past is the part that comes after the workflow clicks.

Tailwind is usually sold as a simpler way to write CSS, but it’s never felt simpler to me. It feels like a layer sitting on top of CSS that quietly implies some of the hard parts went away. They didn’t. You still have to understand layout, spacing, specificity, responsive behavior, focus states, accessibility, and how a browser actually paints the thing.

Tailwind gives you a new syntax and a new mental model to learn on top of all of that, not instead of it.

Before the complaints, I want to give Tailwind its fair case, because “CSS is hard and this is easy” is not the best argument for it.

Why people like Tailwind

Plain CSS can become hard to maintain in ways most developers recognize.

Global stylesheets can turn into specificity fights. You write one selector to outrank another, and a year later nobody can touch anything without worrying that something will break somewhere else. I once inherited a marketing site where half the overrides were !important stacked on top of other !important, and unpicking it was slow, annoying work.

Stylesheets also collect old rules nobody wants to delete, because you cannot always prove a selector is unused. And naming is a real tax. “What do we call this wrapper?” is a dumb thing to spend twenty minutes on, but teams do spend twenty minutes on it.

Tailwind avoids a lot of that. Utilities don’t fight each other the same way cascading selectors do. Styles live on the element, so when you delete a component, you usually delete its styling with it. The generated CSS also doesn’t grow in the same way a traditional stylesheet can, because once a utility exists, it exists.

I don’t think any of that is wrong. Putting styles next to markup solves real problems that “separation of concerns” created in the first place.

My problem is that the problems don’t disappear. They come back in a different form.

The mess just moves

The first thing you notice is the noise. A single button can end up looking like this:

And that’s before a single line of logic.

In a React or Svelte component that already has props, conditionals, ARIA attributes, and event handlers, that class string is one more dense thing fighting for attention. You open the file to understand what the component does, but first you have to scan through a long list of visual decisions.

We keep saying code is read more often than it’s written, and then we write components you have to squint at.

Refactoring is where this stops being just an aesthetic complaint. On one project, we tightened the spacing scale by one step. That meant looking for every py-4 that should become py-3, every gap-6 that should become gap-4, and so on.

There is no safe find-and-replace for that, because py-4 on a card and py-4 on a toolbar do not mean the same thing. The class string cannot tell you the intent.

Tailwind has @apply, but if you start using it everywhere, you are basically rebuilding the semantic stylesheet you were trying to avoid. Only now it lives inside the Tailwind workflow, which never feels like the most natural place for it. And even the people who build Tailwind tell you not to lean on it.

So people extract components. Or they create shared class-string constants. Or they add helper functions and variant abstractions.

That can work. Sometimes it’s the right thing to do. But it’s still structure. It’s still architecture. It’s still the same kind of design work you would have done elsewhere, just with a different set of tools.

Conditional styling makes this more obvious. A couple of state-dependent classes are fine. But once your styling depends on props, breakpoints, hover, disabled states, dark mode, and data attributes, the class list stops being a list. It becomes logic written as strings.

That’s why so many Tailwind codebases grow a cn helper and pull in tailwind-merge. Two utilities can quietly set the same property, and the one that wins is not always the one you expected. By the time you have clsx, tailwind-merge, and a variant library like CVA, Tailwind is not “just utility classes” anymore.

It’s a small stack of conventions and tooling the team has to choose, agree on, document, and keep alive.

You still have to design the system. The work did not vanish. It moved into config files, component APIs, helpers, and naming rules.

“Glorified inline CSS” is not quite fair, but

People call Tailwind glorified inline styles, and I don’t fully agree. The real...

tailwind thing work like still adds

Related Articles