The Case for Tailwind CSS

A Brief History

Before components were invented, we had component-like objects.

<div class="style"></div>

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

We “named” these pseudo-components by giving them semantic CSS classes.

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

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

Eventually, real components came along and formalized the abstraction.

function Card() {
  return <div className="style"></div>;
}

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

We no longer needed CSS classes to act as component names. They were free to resume regular duty as an API between markup and styles.

// Markup  <-->  API  <-->  Styles
   <div>       .style       background: black
                            color: white

But instead of using this opportunity to design a better API, we carried on with semantic classes.

function Card() {
  return <div className="card"></div>;
}

function Label() {
  return <div className="label"></div>;
}

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

.label {
  color: black;
  font-weight: bold;
}

Semantic Classes are a Bad API

They don’t abstract anything away. (For example, I don’t know what .card does until I read its implementation.)

Each class can have multiple effects. Different classes might have the same effect. Classes are not easily composable.

And worst of all, new classes are added freely, which means the API surface can grow unbounded.

By writing CSS like this, we are building out an ad-hoc, undocumented, undiscoverable API. For any other code, this would be unacceptable. Why treat CSS differently?

The Functional Future

In Tailwind—and more broadly, functional CSS libraries—each class is an abstraction over a UI primitive. They represent things such as color (.bg-black), size (.h-8), whitespace (.p-2), and typography (.font-bold). In theory, they comprise a vocabulary expressive enough to describe any UI.

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

function Label() {
  return <div className="text-black font-bold"></div>;
}

Designs are implemented not by adding new classes, but by composing existing ones. This makes the markup verbose, but in return, keeps the API finite and well-defined.

The API is fully documented and learned just once. Changes are localized to the markup. Deleting code is easy.

Anecdotally, I’ve noticed that Tailwind codebases are easier to maintain. When revisiting old code, or reviewing new code, I don’t struggle to understand the API between markup and styles.

Semantic CSS was necessary before components existed. Nowadays, I consider it outdated and harmful, and avoid it when possible.

Trade-offs

More thoughts...