The Case for Tailwind CSS

The Semantic Past

Remember when we were told to make CSS classes “semantic”?

<div class="card"></div>

<div class="avatar"></div>

.card {
  background: black;
  color: white;
}

.avatar {
  background: black;
  border-radius: 100%;
}

I’ve written a ton of code like this. Most likely, you have too. And yet, even with strict naming (BEM) and scoping (CSS modules), I’ve never seen it age gracefully. As the code grows, it gets harder to work with. Deleting old CSS is especially perilous.

Why does this happen? Maybe “best practice” is actually bad in the long run?

Let’s dig into how semantic CSS works. Each class is an interface that links markup to corresponding styles. When taken as a whole, the classes form an API layer between markup and styles:

// Markup   // API    // Styles

    <div>   .card     background: black
                      color: white

    <div>   .avatar   background: black
                      border-radius: 100%

What are some characteristics of this API?

All hallmarks of a bad abstraction.

Now let’s continue implementing the UI. To represent new objects, we need to create new classes. This causes the API surface to grow unbounded:

// Markup   // API              // Styles

    <div>   .card__label--sm    font-size: 90%

    <div>   .card__label--md    font-size: 100%

    <div>   .card__label--lg    font-size: 110%

    <div>   .avatar__name--sm   font-size: 90%

    <div>   .avatar__name--md   font-size: 100%

    <div>   .avatar__name--lg   font-size: 110%


    Etc...

In a few weeks, we’ll find ourselves flipping back and forth between HTML and CSS files, trying to understand the API before we can even work on the code.

The Functional Future

Tailwind—and in general, functional CSS libraries—provide a finite API.

Each class is an abstraction over a UI primitive, not a UI object. They represent things like color (.bg-black), size (.w-8), whitespace (.m-2), and typography (.text-sm).

Instead of creating new classes, we compose them. The number of classes is finite, but the combinations are essentially infinite. This gives us the expressive power to implement any UI:

function Card() {
  return <div className="bg-black text-white"></div>;
}

function Avatar() {
  return <div className="bg-black rounded-full"></div>;
}

What are some characteristics of this API?

All hallmarks of maintainable code.

In Practice

Like many others, I needed time to unlearn old “best practices.” It was hard to believe that we should replace “clean” semantic classes with “illegible” markup.

But, once it clicked, Tailwind completely changed how I write and maintain CSS. I now consider semantic CSS harmful and avoid it whenever I can.

Some Trade-offs

As with any library, there are trade-offs.