Why tailwind is bad


So unlike styled-components, there’s actually quite a few things that I appreciate about tailwind, so despite the click-baity title up here, this post should be more balanced in its critique than the last one.

Why it’s good

Tailwind is an amazing at one thing, and that is at being a design framework. You see, when an expert CSS designer starts a project, they will always establish a set of primitives from which to build their design. Those are a spacing unit, a color palette, shades for each color, and a set of component sizes, minimally.

The problem with this approach has always been that most developers are not expert CSS designers; they’re average CSS designers who are tasked to translate mockups into code, when it’s not just designing stuff themselves. So lacking this experience, we skip the whole CSS base design system phase and jump to coding. And that’s why we end up seeing color: #e07b7b (and 10 different other shades) instead of color: $danger-600. It’s because most devs don’t have the experience or time to do CSS from scratch properly.

And that’s where tailwind comes in. As I said, it’s an amazing design framework and being able to ensure that all the juniors are going to produce somewhat consistent styling with low supervision is a huge benefit. Every app should have a design framework, even if it doesn’t have tailwind.

Why it’s bad

This below is a basic user card. I wrote most of it myself, in about 20 minutes. Not great, but for someone who isn’t a designer, definitely acceptable for an MVP. This looks fine.

Leanne Graham
deactivated
#web
#ML
#engineer
I revolutionize end-to-end systems by transitionning cutting-edge web services to aggregate real-time technologies.

What doesn’t look fine is this:

<div class="xs:max-sm:w-full w-96 xs:max-sm:rounded-xl rounded-2xl bg-white border-zinc-100 dark:bg-zinc-900 border dark:border-zinc-700">
  <div class="flex flex-col gap-2 p-8">
    <div class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white flex flex-row">
      <span class="mr-2">Leanne Graham</span>
      <div class="inline-block text-sm tracking-tight bg-zinc-200 border-zinc-300 dark:bg-zinc-800 border dark:border-zinc-600 py-1 px-2 rounded-md mb-0.5">deactivated</div>
    </div>
    <div class="flex flex-row flex-wrap">
      <div class="inline-block rounded-full px-3 py-1 text-sm font-semibold bg-gray-700 text-gray-200 dark:bg-gray-200 dark:text-gray-700 mr-2 mb-2">#photography</div>
      <div class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">#travel</div>
      <div class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">#winter</div>
    </div>
    <div class="font-normal leading-6 text-gray-700 dark:text-gray-400 mb-0.5">I revolutionize end-to-end systems by transitionning cutting-edge web services to aggregate real-time technologies.</div>
    <label class="flex cursor-pointer items-center justify-between p-1">
      <span>Receive updates</span>
      <div class="relative grid place-content-center">
        <input type="checkbox" class="peer h-6 w-12 cursor-pointer appearance-none rounded-full border border-gray-300 bg-white checked:border-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-900 focus-visible:ring-offset-2" />
        <span class="pointer-events-none absolute start-1 top-1 block h-4 w-4 rounded-full bg-gray-400 transition-all duration-200 peer-checked:start-7 peer-checked:bg-gray-900"></span>
      </div>
    </label>
    <div class="row gap-1">
      <button class="inline-block cursor-pointer rounded-md bg-gray-700 px-4 py-3.5 text-center text-sm font-semibold uppercase text-white transition duration-200 ease-in-out hover:bg-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-700 focus-visible:ring-offset-2 active:scale-95">Like</button>
      <button class="inline-block cursor-pointer rounded-md border border-gray-700 bg-transparent px-4 py-3.5 text-center text-sm font-semibold uppercase text-white transition duration-200 ease-in-out hover:bg-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-700 focus-visible:ring-offset-2 active:scale-95">Mute</button>
    </div>
  </div>
</div>

The tailwind website tries to gaslight us into thinking that it’s fine to write your styles like this. That’s bullshit. This is a wall of text, this is not markup code. It’s as if all of a sudden we go back to the dark times where newlines hadn’t been invented yet. To illustrate their point, they sure give a few nice examples here and there. But those examples are never complete. If you’re building a modern website, you also need responsive design, so all the dimensions classes (w-full, h-16, etc) are double or tripled with additional sm:... and lg:... prefixes. And light+dark themes are gaining support, so you also need to double your color classes (text-gray-700, bg-blue-100, etc) with dark:.... The example above is a realistic one, and it can get much worse than that.

I made the mistake of purchasing TailwindUI for a new project some time ago. The initial productivity gain is amazing and it looks great initially, but the moment you start trying to make changes, you start reconsidering your life choices. You see, if I wanted to update the card’s “deactivated” status above in a BEM project, I’d quickly locate the card__status and proceed to update the class. With the monstrosity above, I need to locate the inline-block text-sm tracking-tight bg-zinc-200 border-zinc-300 dark:bg-zinc-800 border dark:border-zinc-600 py-1 px-2 rounded-md mb-0.5 element. And then when you start updating it, blink for a moment and you might very well loose track of where the element was because it’s drowned in a sea of inline styles. And if you need to scroll around to refer to styles a few lines away, you better keep a second window open because there is no way you’re finding that element again. Using tailwind this way is basically inline styles and it’s wrong. You get an amazing DX for the first D who writes the MVP component, and every subsequent D that follows gets the worst DX ever since inline styles.

And that’s also what you get everytime you open your devtools. Your DOM tree is nothing but a downpour of tailwind classes that overflow the already cramped panel. If I open my DOM tree, I want to see that this div is a card__title or a list__item. Seeing a [insert stream of tailwind classes] does nothing to tell me what kind of element I have in front of my eyes, be it in the devtools or in the source code. DX isn’t about what feels best to write fast, DX is about what gives the best experience over the whole period during which the code needs to be read, debugged & updated.

The other failing of tailwind is that, if used as intended by its creator, the styles aren’t shared. If I want to build a “button” style, there is no obvious recommended way to build it from say bg-blue-500 text-white font-semibold active:bg-blue-400 and share it across many locations. So what ends up happening is tons of duplication. Wanna update button-like styles? Go hunt down all those duplicates and update them one by one.

Being able to give good semantic names is one of the most important aspects of programming. As a wise stackoverflow user once said, “the process of naming makes you face the horrible fact that you have no idea what the hell you’re doing”.

Summarizing

So if we recap why tailwind is bad:

  • It makes it hard to read the element tree
  • It makes it hard to update styles
  • It doesn’t recommend a way to share styles

How it can be good

So that being said, I’d like to repeat again (because I’m sure I’ll get angry comments on it) that tailwind is a great CSS framework. It enforces base style consistency, and that’s a good thing. If you write CSS without a framework that fullfills the same role as tailwind, you’re doing CSS wrong.

But it feels like tailwind missed a step. That step, which I came across recently, is daisyUI. It turns your code from something like this:

<button class="bg-indigo-600 px-4 py-3 text-center text-sm font-semibold inline-block text-white cursor-pointer uppercase transition duration-200 ease-in-out rounded-md hover:bg-indigo-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 focus-visible:ring-offset-2 active:scale-95">
  Button
</button>

Into something like this:

<button class="btn btn-primary">
  Button
</button>

This here is simple, readable & reusable code.

The problem with tailwind classes is they are the next step right above inline CSS styles. They’re a good design language, but they’re not enough to build a design system. If you’re building a design system, you want good semantic names. If I add the style bg-blue-500 on my button, I want to be able to tweak that color later. The actual color name I will want when it’s time to update the styles is something like bg-primary-500 or bg-info-500, because it provides a semantic value and can be tweaked separately when the designer suddenly decides that our primary color now needs to be flashy pink because all his friends are using it (or maybe because they’re all not using it).

The other acceptable way to use tailwind that I’ve found is to use it with regular CSS, update the tailwind config to get semantic color names, and use @apply:

.btn--primary {
  @apply text-white;
  @apply bg-primary-500;
}
<button class="btn btn--primary">
  Button
</button>

You still get simple, readable & reusable code.

“But what if I need to add a margin to some buttons, won’t I need to create tons of modifiers for each and every element?”

Well no. You see the goal is always to have readable & reusable code. If the best way to reach that goal is to use an occasional tailwind class here and there, you just use it:

<button class="btn btn--primary mb-1">
  Button
</button>

We did this in a team I worked with a few months ago, and our non-enforced rule was “if you need more than 3-4 tailwind classes, you should probably make it a new class”. Worked well, we got the best of both worlds.

So please, don’t ever use tailwind again as tailwind says you should use it.