back to Jitbit Blog home About this blog

I really wanted to like Tailwind CSS

by Alex Yumashev · Updated Jul 24 2023

TL;DR

Nobody:

Absolutely no one:

Me: Here's what I think about Tailwind CSS!

First, a tip of the hat

Let's get one thing out of the way: Tailwind CSS is great.

For starters, Tailwind is a very polished and well-thought-out product. As a fellow bootstrapper - I tip my hat. The docs are amazing, the examples are great, Tailwind UI Kit is a life saver and the "Refactoring UI" book is a must read. And I'm an extremely satisfied paying customer for Adam's & Steve's stuff.

Tailwind is also great as a tool - it's a perfect UI builder for new projects. Creating stuff from scratch with Tailwind is just ah-amazing. Heck, I have actually just redesigned this very website using Tailwind. You're in the flow, in the zone, wired in, out of this world, creating stuff.

Think of it as a visual editor - "Figma for developers" - a graphics design tool without leaving your code editor. Playing with default classes, trying stuff, re-trying stuff, then throwing in some more stuff... Hey, even Tailwind's landing page advertises this very approach in their hero video. You start with a bunch of unstyled mess and work from there. Sounds grrreat.

And those are exactly the two main reasons developers loooooove Tailwind: because (A) - developers love writing and rewriting everything from scratch (oh yeah, writing code is so much easier than reading code). And (B) - developers love doing stuff without leaving their code editors. And also, maybe (C) - developers tend to think about UI at the last minute. "Hey, I just hacked a cool project, now I need some UI for it - what can I use to turn this messy Times-New-Roman ugliness into something decent, fast?"

I get it, not everyone is an experienced front-end dev (I'm surely not one!), who loves polishing and re-polishing the UI, pixel by pixel, color shade by color shade... Screw that, just give us a flexible system with some nice-looking defaults.

However

Looking at what Tailwind has grown into by version 3.2, I have some concerns against using Tailwind in big projects.

Tailwind takes up 70% of my markup...
...even after I'm done with the design

While Tailwind makes writing CSS fast and enjoyable, the biggest problem is that once I'm done with CSS - Tailwind is still in my face. Staring at me even after I'm done with the design. Making everything that's not CSS - content, JS, markup etc - much harder to work with. It takes valuable space, and going back to a file that looks like this causes me physical pain:

<div class="min-h-full bg-white px-4 py-16 sm:px-6 sm:py-24 md:grid md:place-items-center lg:px-8">
  <div class="mx-auto max-w-max">
    <main class="sm:flex">
      <p class="text-4xl font-bold tracking-tight text-indigo-600 sm:text-5xl">400</p>
      <div class="sm:ml-6">
        <div class="sm:border-l sm:border-gray-200 sm:pl-6">
          <h1 class="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">Error</h1>
        </div>
        <div class="mt-10 flex space-x-3 sm:border-l sm:border-transparent sm:pl-6">
          <a href="#" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">Go back home</a>
          <a href="#" class="inline-flex items-center rounded-md border border-transparent bg-indigo-100 px-4 py-2 text-sm font-medium text-indigo-700 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">Contact support</a>
        </div>
      </div>
    </main>
  </div>
</div>

I just wanted to change that "Error" header text, now where is that exactly? Do you see it?

Look, Tailwind, I like you, we've had a great time together, but I'm done with the design, I'm moving on, stop staring at me and get out of my face.

HTML pollution is not a huge problem when coding landing pages and websites like this one you're reading right now. Because marketing websites are 99% design and only 1% code (and that 1% code all sits in the "contact us" form). But in a complex app with with hundreds of thousands LOC...

Maintainability & discoverability

Let's say you already have a huge app and you need to fix a minor UI bug. Some button looks odd or something.

Since Tailwind is an abstraction over CSS, it adds an extra layer to reverse engineer. Which is what debugging essentially is - reverse engineering your own code. Working backwards from an unexpected result to the reason behind it.

Say, you haven't touched your CSS in months. You can't just "remember" what might be wrong with that button. So you fire up your dev-tools in the browser, right-click the offender, "inspect element" and notice some strange styling that comes from... "output.css". But where the heck did it originate from exactly? Is it a TW utility class we used on the element via @apply? Is it the "forms" plugin? Is it the "prose" plugin? Did someone override the default "theme" in "tailwind.config.js"? Or may be it's the default "preflight" base styling that TW applies? And where is that configured exactly, the "tailwind.config.js"? NOPE, turns out our new developer has added a @layer override in "input.css". OK, I think I found the reason, let's try the fix now! Opening my HTML, locating the element in a wall of utility classes, applyting the fix, done.

Refreshing the browser. Wait, nothing's changed. Was it the wrong fix? Maybe. Oh, wait, maybe it's the npm-based "JIT" watcher that's not working? Let me see... Yep, it's down. The npm process has either crashed again with the "JavaScript heap out of memory" error. Or maybe the npm-script does not work on WSL2 because I'm currently on my Windows laptop. Or maybe I'm using a code-editor that we haven't configured the build system for (we have it for VsCode and VS-2022, but not for Sublime). Anyway, let's just launch it manually (googling-googling-found it) npx blah-bleh --WATCH thanks StackOverflow. I really hope the fix works, because I don't want to start over.

By the way, did I mention that Tailwind CSS is the only freaking reason we have npm in this project? Yeah, not everyone uses JavaScript. People also use C#, Java, Rails, Go, Python and even (wait for it...) PHP. We're a dotnet shop, we already use nuget, libman, web-compiler, hell, why not throw npm into the mix. With "package.json", build scripts and a 13MB "node-modules" directory...

Integrability

Let's say you'd like to try to integrate Tailwind CSS into a big existing project.

  1. The project is big. It has half a million lines of code in 4087 files. Around 500 of those are front-end markup files - HTML views and partials (ouch!)
  2. The project already has a theme + design system, also brand-colors, icons (ouch #2)
  3. ...and a default font, visual hierarchy etc etc (ouch #3)

You basically have two options:

  1. Rewrite everything from scratch (of course)
  2. Gradual "incremental" rewrite, starting small and letting Tailwind take over eventually

Let's try option 2. Say I'd like to redesign that ugly looking button. After all Tailwind is just a bunch of utility classes, should be no problem isolating it from the rest of the app, right?

Installing Tailwind... done. The project looks awful, obviously, but that's fine. Tailwind has added its default font and a CSS normalizer. Let's override the default font back. Let's also make our existing CSS "Tailwind-friendly" so it does not conflict with all the default box-sizing, height:auto, margins, colors, let's add back the bold-font to all the H1-H6, remove default outlines etc.

If that sounds like too much work we can always just disable TW "preflight" completely, but this way the utility classes might look weird.

OK, we've added Tailwind, set up the tooling for all the devs (some use VSCode, some use Webstorm, or Rider, or the "big" Visual Studio), and after only 2 days our app looks more or less the same as before.

Our project is not heavily "componentized", if we have a button in our code - then it is... just a <button>, not a partial or a component. Sometimes we might add <button class="inactive"> but that's about it. I just ran a search, and we have 378 <buttons> in our code. If I want them all to have rounded corners, I would either have to copy-paste rounded-md 378 times, or convert all my buttons to a component/partial, which sounds like an overkill.

Let's go with the @apply directive then. That's exactly what it's for, according to the Tailwind docs "creating a partial for something as small as a button can feel like overkill". Exactly! I'll just rewrite all my buttons using @apply. And now we have basically ended up with a CSS file (again) that uses TW-utilities instead of the "regular" CSS. We now have button { @apply px-4 py-2 rounded-md } instead of button { padding: x y; border-radius: z; }. Good. I don't get how is that better than regular CSS, tho. We still have an external CSS file, we still describe all our elements in it, just with a different syntax now. What have we gained? Aside from the Tailwind's constraints (in a good way) and the ability to use Tailwind UI kit that we paid for.

"You just don't get it, boomer"

Yeah, I must be old. So I went out and searched Hacker News for all the TW-related comments and then went on and listened to 5 podcast episodes. These are the benefits developers mention all the time:

1. Developers (apparently) hate having a separate CSS file They really do, as it turns out. This is a surprise to me. But, well, OK, I'll take that as a valid reason. Kinda ruined if you use @apply tho.

2. Developers miss the native-app dev experience a surprising number of people have literally mentioned this, including Adam Wathan himself. Coming from the native development world (writing desktop or mobile apps for Mac, PC, iOS etc) to the web - they miss the ability to simply click a UI element in the IDE (like a textbox) and then edit its properties in the side bar (bold font, black background etc). Without having to come up with an identifier or a class for the element. Without going to a separate css-file and target the identifier's selector. Without writing actual CSS. Without keeping cross-browser nuances in mind, along with the cascading nature and the "not repeat yourself" rule.

3. Devs hate naming stuff and coming up with class names takes a significant part of their day. Everyone mentions that. A lot.

All valid points, I guess. However, we don't write CSS every day. I just took our LESS file (yeah, we still use LESS) and pulled up the git history for it. In the last 10 years (yeah, we're that old) it's been modified once every two months on average. See, our product is a long-lasting, profitable, boring app. We get back to CSS only when something breaks or something needs a facelift and that's it. We're OK with having it in a separate file, and coming up with a class name once every two months is not a huge problem.

Tailwind is an ORM

While analyzing Tailwind's pros and cons I realized it kinda reminds me of another "tool" - an ORM. ORM libraries help with database access, so you don't have to "write SQL". They're are great to jump-start an app, build a landing page, a prototype, an MVP, to test a hypothesis, hey, even power up a working product with paying customers for a couple of years. But once your app gets mature and scales up to millions of daily active users - ORM quickly becomes a pain for an experienced DBA. You know, the type of DBA who sees a way to optimize a database query just by glancing at its execution plan XML. You're in deep waters now, you're in the land of database sharding and distributed caches. And now the ORM just gets in the way. Our DBA spends more time fighting it, instead of throwing away an abstraction layer and make things simple again.

(or wait until someone releases a light weight, non-opinionated "micro" ORM, like "Dapper" from Marc, Nick and Sam - the guys who wrote that website we all use every day called StackOverflow)


Coming up in the "grumpy old dev" series: how to build a static website without React + Next.js + build-step + Docker.