Is Tailwind CSS Worth It?

Take a deeper look at utility classes in CSS.

CSS utility classes have been around for a long time now. However, their popularity has recently skyrocketed with the adoption of libraries like Tailwind CSS. In this article, I want to share my thoughts, opinions, and hot takes about how tools like this change the user experience for developers and alter our relationship with the DOM. Feel free to completely disagree with me!

What Problem Are We Trying To Solve?

In my view, the goal of a utility class-based system is to decouple. You can decouple your component styles from the HTML structure. You can decouple your rules from grouped component styles. At first glance, this sounds great, and it is. However, this methodology comes with its own set of problems.

CSS utility classes, or Functional CSS, are all about thinking small. The goal is to break up your app into a ton of little tiny classes that do one thing. You may have heard of a similar concept in programming called the Single Responsibility Principle or Functional Programming. In CSS, the same concept exists. For example, they might center text in a div with a class called .text-center. These tiny classes are then composed together on a DOM element until the desired result is achieved.

It sounds straightforward at first but, while focusing on the micro we lose sight of the macro. In this interview, Sarah Dayan gives an amazing walk down memory lane about the recent history of CSS. She elegantly describes not only the technologies that were used but, expands on the philosophy behind them. Even though this video is a few years old now, I think it's still relevant. She leads us to a logical path of using Atomic CSS and Design Systems to build modern websites and apps. She's not wrong.

She is also currently found on the homepage of the Tailwind CSS website. There she is quoted as saying that Tailwind is the only framework that has scaled on large teams. I think she makes some great points but misses a crucial one. Tailwind is UGLY! I'm not the first person to say this. The internet has plenty of places you can find critiques of Tailwind.

I'd like to focus on the actual UX for developers who use it. When asked about the ugly HTML that is generated by a pure utility class approach, even she was backed into a corner around the 14 min mark and was forced to admit that "you can get used to almost anything". My question is, should we?

Abstraction reduces cognitive load

The UX of Utility Classes

I worry that sometimes as developers we get so obsessed with DRY principles and smaller bundles, that we forget our own needs as the people building this stuff. Ease of use, productivity, and general happiness while using the methodology matter. I don't care what the bundle size looks like if it makes my life much harder. We focus on the objective more than the subjective. Which makes sense, until it doesn't. There's a reason why we don't create web apps in raw JavaScript. It's tedious, hard to read, and hard to organize. This is how I feel about a pure CSS utility class approach.

Imagine having to order your dinner by listing all of the ingredients instead of just saying the name of the dish. For example, you could sit down in a restaurant, wait for the waiter to come over, then say I want a "traditional Italian meal consisting of long thin wheat noodles, tomatoes that have been turned into a liquid, and small chunks of beef that have been shaped into spheres". This is how utility classes work. I'd rather just order "spaghetti and meatballs" and call it a day. 🤷‍♂️🍝

In addition to producing some headache-inducing HTML templates, this style of CSS encourages less semantic HTML. Don't take my word for it though, go look at the Tailwind CSS website. I see lots of "div soup" in the examples. Here's a sample of what I'm talking about from their website. Do you have any idea what this is without rendering it? I sure don't.

<div class="space-y-2">
  <div class="relative">
    <div class="bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
      <div
        class="bg-cyan-500 dark:bg-cyan-400 w-1/2 h-2"
        role="progressbar"
        aria-label="music progress"
        aria-valuenow="1456"
        aria-valuemin="0"
        aria-valuemax="4550"
      ></div>
    </div>
    <div
      class="ring-cyan-500 dark:ring-cyan-400 ring-2 absolute left-1/2 top-1/2 w-4 h-4 -mt-2 -ml-2 flex items-center justify-center bg-white rounded-full shadow"
    >
      <div
        class="w-1.5 h-1.5 bg-cyan-500 dark:bg-cyan-400 rounded-full ring-1 ring-inset ring-slate-900/5"
      ></div>
    </div>
  </div>
  <div class="flex justify-between text-sm leading-6 font-medium tabular-nums">
    <div class="text-cyan-500 dark:text-slate-100">24:16</div>
    <div class="text-slate-500 dark:text-slate-400">75:50</div>
  </div>
</div>

Can you imagine quickly scanning this HTML file to find the correct div to update with this lack of context? By context I mean, when you look at the template, you know what it is. Tailwind goes against the old Object-Oriented CSS patterns and exchanges them for total anarchy. Heaven forbid were you to find a CSS bug. You might have to adjust classes on several elements before finding the exact .px-3 class you need. As humans, we need abstract concepts to use as mental models to save time and do value mapping in our brains. The infamous .card class which has been used in several different CSS frameworks and apps, was hugely popular because it scratched our primal itch to attach meaning to a thing. Pure utility class templates throw all that out the window. The Best of Both Worlds

OK, let's get to the less negative part of this article. After all, I do like the idea of making things atomic! I just don't think that classes are the place to do it. We really can make better design systems without reinventing the wheel with every new component. If you want to get Atomic, you need to go even smaller than classes. You need to use CSS Custom Properties.

I recently discovered a library that perfectly sums up my thoughts on this. It's called Open Props and I highly recommend you check it out. It solves both problems elegantly. How do I create meaning and abstractions in my HTML while also creating global styles that are reusable?

You can see in the Open Props examples that they mostly use meaningful class names. Which leads to better "scannability" in our HTML and faster troubleshooting. Take this example:

.hero {
  line-height: var(--font-lineheight-1);
  font-size: var(--font-size-fluid-3);
  font-weight: var(--font-weight-9);
  font-family: var(--font-sans);
}

Everyone knows what a "hero" element is. With a pure utility class approach, this would become 4 separate classes to maintain the individual element. Now, they're grouped in one spot which makes it easier to read and understand. Oh and by the way, it won't add extra noise to your framework-specific DOM attributes such as Angular's *ngFor. This also allows you to continue using other class naming conventions such as BEM or CUBE. The choice is yours. To be fair, you could even combine this with utility classes if you prefer. However, I still argue that you're making a big trade-off by doing so.

.fs-lg {
  font-size: var(--font-size-8);
}
<div class="fs-lg">Hello World</div>  <-- you lose the meaning again

Conclusion

To wrap things up, I just want to reiterate that I support building more global styling rules that help us build web apps in an "atomic" way. I just don't believe classes are the place to do it. Tailwind is probably not going anywhere and we need to accept it but, I don't want to lose sight of what we're losing in the process.

I am excited about libraries like Open Props which might be the best way to find a middle ground between reusability and meaning. I hope this article gave you some food for thought when choosing you're next CSS framework.