I’ve seen Tailwind dismissed as unreadable garbage and praised as the best thing since sliced bread. After three years of professional use, I think both sides are equally right and mostly talking past each other.

The opposition is often arguing based on taste: developers look at a wall of utility classes and get a funny feeling in their stomach. And yet, advocates rarely make a convincing case for why that discomfort is worth it. There are many good pro and contra arguments for Tailwind and I think both camps could benefit by putting emotions aside and acknowledging its trade-offs.

I reach for it when it fits, and leave it when it doesn’t, and I think you should, too. I’ll share how to tell if it’s the right call, but let’s first go through the pros and cons of Tailwind to establish a baseline.

What’s so good about Tailwind

Locality of Behavior

Imagine you’re buying groceries, and you want to know the price of an item. With conventional CSS, the price would be on a shelf tag, hopefully fairly close to the product. You have to look at the item, then look somewhere else to get the information you need.

Tailwind takes a different approach: the price is printed directly on the packaging. If you look at the item, you immediately know everything you need to know about it, no second location required.

Minimal cereal shelf with fictional boxes and number-only price stickers on each package

The engineering principle applied here is called locality of behavior.

Tailwind allows you to style elements directly in your markup, so you don’t have to dig through stylesheets or jump between CSS and HTML sections, which can be confusing and time-consuming, even if they’re in the same file.

Naming no more

Phil Karlton once said: “There are only two hard things in Computer Science: cache invalidation and naming things.”

Thankfully, CSS itself doesn’t mess with caching, so there is nothing to invalidate. It however involves lots of naming when applying classes to elements, forcing you to pretend that a box added purely for presentation is a concept worthy of a meaningful name. Consider this example:

<div class="field">
    <label class="field__label">Search</label>
    <!-- What should we name the following div? -->
    <div class="???">
        <input class="field__input" />
        <svg class="field__icon">...</svg>
    </div>
</div>

There is no value in naming the div with the question marks. In fact giving it a name somehow implies that it is meaningful, but in reality it just exists to add some flex properties.

With Tailwind you can sidestep this waste of time and brain-power. Since the classes in Tailwind are the styles, there is nothing left to invent. Just apply the utility classes you need and move on.

Natural Consistency

Tailwind makes consistency easier than regular CSS by giving you a predefined token system for spacing, typography, colors, shadows, border radius, and more.

Using p-4, rounded-lg, or opacity-75 feels natural because they’re the defaults. Going off script with something like p-[5px] is possible, but it feels wrong. That little bit of friction is what keeps people and coding agents aligned without blocking them, naturally reducing drift.

Zero Specificity Headaches

Using Tailwind, most of the time you won’t have to deal with CSS selector complexities, including specificity. You don’t have to mess with ids, nested selectors, nth-child or element selectors, as Tailwind only uses classes for everything and classes all have the same specificity.

And if you use tailwind-merge, then you can reduce the ambiguity of “What class will be applied here?” as well, as you can ensure that always the last class that is mentioned for a given property is applied:

twMerge('text-md text-sm text-lg text-xs'); // text-xs will be applied as it is the last one.

Minimal Production CSS

Tailwind keeps your production CSS minimal. Its build process scans the codebase for used Tailwind classes and removes any it didn’t find from the final bundle. This reduces the output drastically for most projects, going down from multiple megabytes of tailwind classes, to a few kilobytes.

What Sucks about Tailwind

Classes, Classes, Classes, Src, Classes

I mentioned earlier how convenient it is to read the price right off the item. That works beautifully for just a price. But in reality, HTML elements are packed with more information. Tailwind classes and div-soup can suffocate important attributes in your markup like alt, src or aria-label and make it an unreadable mess, turning the beloved locality of behavior into a locality of noise.

// Is this image lazy loaded or not?
<img src="organic-chicken.jpg" alt="Free-range organic chicken"
className="block w-full h-48 md:h-64 object-cover rounded-t-xl
border-2 border-amber-300 bg-white p-0.5 shadow-lg hover:shadow-2xl
hover:scale-105 transition-all duration-200 ease-out dark:border-amber-600
dark:bg-gray-800" loading="lazy" decoding="async" />

It’s Complex And Inconsistent

Tailwind promises to reduce cognitive load, but actually introduces lots of it. The upfront learning curve is steep.

One would wish that they just used CSS namings for everything, but they instead named line-height leading and letter-spacing tracking.

But Tailwind doesn’t stop with weird namings. They also introduced things like square bracket syntax for arbitrary values (p-[5px]), negative prefixes that look like CLI flags (-mx-4), bang-syntax for !important (!font-bold), and magic group and peer modifiers that force you to tag parents or siblings to react to their state. All of these features are useful and important, but every one of these is its own micro-syntax you have to memorize, turning what should be simple styling into a sea of additional complexities you have to learn.

It’s a Leaky Abstraction

People getting into Tailwind often think they’ll never write style="" again. That is sadly not true. There are plenty of situations where using classes simply isn’t possible. For example, the Tailwind compiler only ships classes it actually sees in your codebase, so a dynamic class like h-[${elementHeight}px] won’t work. You’re forced to reach for the style attribute, or worse, a safe-list workaround where you maintain a string with all the possible values it could have, just to trick the scanner:

// h-[1px], h-[2px], h-[3px], h-[4px], ...

And it also isn’t up-to-date with the newest CSS features.

At the bottom of this post you can find a scroll-driven “Copy link” button animation that, at time of writing, only works in Chromium-based browsers. If I had used Tailwind, I would have had to write a plugin or custom CSS, to get this behavior, dealing with additional complexities or leaving the benefits of the abstraction behind.

Dependency Overhead

You will probably not just install tailwindcss and @tailwindcss/vite, but also packages like tailwind-merge or @tailwindcss/typography.

Tailwind comes with dependencies. This probably isn’t a big deal for you, but maybe it’s a good idea to have fewer dependencies if we have a new npm worm like every two weeks.

Build Step Required

Tailwind needs a build step. Now, if you work with something like React, then this will be a non-issue. But let’s say you are working in a weird web-page builder plugin inside Wordpress and you are editing some markup directly. Using Tailwind wouldn’t work in that environment without introducing other major drawbacks.

When to use Tailwind

All in all I don’t think Tailwind is perfect, but I also don’t think it’s bad. There are situations where its use is valid and there are situations where it isn’t. Now that we’ve got the pros and cons out of the way, let’s talk about when to use it.

Prerequisites

A few preconditions should be met before I would even consider it.

  1. Being able to have a build step is mandatory, because otherwise you will have to ship the entirety of Tailwind, which for newer versions isn’t even possible and older versions require like ~3 Megabytes of CSS. For comparison: this whole site including HTML, CSS, Javascript and images is less than 300kb.

  2. Being able to create components is very helpful, which means you want to be using a framework like React, Vue or Astro. Without access to components, you will end up either copying a lot of classes, breaking the engineering principle “Don’t Repeat Yourself”, or you will end up breaking the Tailwind philosophy and creating classes like btn or input, which basically nullify most of the benefits of using Tailwind in the first place.

Speed > Perfectionism

This site doesn’t use Tailwind. The main reason for that is that i am over-engineering it. This is neither sane nor necessary, but this website represents me and the values i hold and i dearly love good UX, good quality and good performance.

I am using many weird features: scroll animations, variable media queries, a weird old monitor overlay, uncommon CSS selectors… All of this is possible to some extent with Tailwind, but it would feel like trying to thread a needle while wearing gloves. It’s possible, but I would much rather do it bare-handed.

Now if you are not like me, and you do normal stuff while simultaneously trying to deliver results quickly, then Tailwind would probably be the better choice for most situations because not having to name things and also not having to find related styles to your markup saves a lot of time, even if the messiness is a real pain.

Team Work

I think teams, especially big ones, should also consider using Tailwind. As stated earlier, it naturally leads towards consistency, which is a bigger challenge, the bigger the team and product gets. Tailwinds limited options are a major blessing in such an environment.

Conclusion

Tailwind isn’t a hero and it isn’t a villain. It’s a trade: noisy markup in exchange for speed and consistency. Use it if that trade makes sense under your constraints, but don’t force it if it doesn’t. The best tool is the one that fits the job, not the one that fits your identity.


I’d genuinely love to hear your thoughts. Please drop your feedback on daily.dev, LinkedIn or Mastodon!