Tailwind CSS adds more complexity than it removes

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:

<button class="inline-flex items-center justify-center gap-2 rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none dark:bg-blue-500 dark:hover:bg-blue-400">

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 difference is the constraint. Utilities come from a fixed scale, and they can express hover, focus, media queries, dark mode, and other things a style attribute cannot do.

That scale is the point.

Which is exactly why arbitrary values give the game away. A codebase full of w-[437px], top-[13px], and bg-[#f7f1e8] has thrown out the thing that made utilities better than inline styles. Now you have ad hoc CSS inside class names, with worse ergonomics and nothing enforcing a scale.

Tailwind will not stop you. It can't.

So the consistency people credit to Tailwind was never just the tool. It was a well-defined theme and a team disciplined enough to stay inside it. Remove the discipline and you get the same random spacing and one-off colors you would get anywhere else, only spelled differently.

Debugging, and the CSS you still need to know

Debugging gets worse in a specific way, and I don't mean that DevTools stops working. You can still inspect an element and see which utilities are applied.

The problem starts when you toggle a declaration to see what it's doing.

With a normal .card-title class scoped to one component, switching off a property shows you what that element does without it. With Tailwind, if you toggle a property on something like px-4, you are changing the generated rule for every element using px-4 on the page.

That is a very different debugging experience.

You were trying to understand one element, but suddenly half the layout moves. So you end up editing the class list by hand, swapping utilities in and out, checking computed styles, or adding a temporary override.

All of that works. It is just more friction. And when you are doing small visual debugging all day, friction adds up.

The other thing is that Tailwind lets you postpone learning CSS rather than skip it.

You can ship good interfaces by memorizing utilities, right up until something breaks in a way no utility explains. Then the bill arrives, because flexbox is still flexbox, the cascade is still the cascade, and the browser never went anywhere.

That's not a dig at anyone using Tailwind. The underlying knowledge just stays mandatory, so the savings are smaller than they look.

It is a tradeoff, not an upgrade

Tailwind is not bad. I would take a tidy Tailwind codebase over a messy global stylesheet any day. For a lot of teams and products, it's probably the right call. If your team ships faster with it and the components stay readable, keep using it.

What I push back on is treating it as an obvious upgrade over CSS.

It does not just relocate complexity. It often adds some: more syntax, more tooling, more conventions to maintain, and more visual noise in the one place I want to read structure.

I would rather write something that says what an element is than markup that recites every visual decision it makes.

And that's where the “glorified inline CSS” jab starts to feel less unfair to me. Not because Tailwind and inline styles are the same thing. They are not. But they both pull presentation back into the place where I would most like to see structure.

Tailwind makes the first version of a screen quicker to build. I won't pretend otherwise.

The question I care about is what the codebase feels like six months later, once the design has changed, three other people have touched it, and those class strings have quietly become the thing you maintain.

That's where it loses me.