Tailwind isn't the answer
Tags: tailwindcss, css, webdev
Published: 2021-10-24 11:33:40

[Tailwind CSS](https://tailwindcss.com) has taken the frontend development world by storm over the last few years. A utility-first library of CSS classes, it promises a new way of styling that's more consistent, maintainable, and faster than writing CSS directly. And for the most part, it delivers on that promise. 

By using Tailwind you're almost guaranteed a single source of truth for all the values you use throughout a project. From typesets to spacing to colours, everything is defined in a single place. Which means that your code stays consistent and you aren't making things up as you go.

This was Tailwind's biggest idea, and the greatest benefit of utility-first CSS as a concept: compose don't create. 

Tailwind achieves this with an extensive library of CSS classes to style _everything_. The idea being that you no longer write any CSS of your own, you compose predefined classes like lego pieces for every single property. 

Developers new to this way of working often have a knee-jerk reaction just from looking at example code. 

```html
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
 Button
</button>
```

There's no denying that Tailwind is hideous. Its creator acknowledges as much right on the project home page. But that's just pedantry, and if the Tailwind way of doing things really was the panacea to all our problems then it would be a very small price to pay.

## The Problem

The problem with this approach isn't that its ugly, or bloated (Tailwind [purges](https://tailwindcss.com/docs/optimizing-for-production) unused classes), or that "you might as well write inline styles" (you shouldn't). It's that in order to apply a consistent set of values with classes, you also have to create classes for _every_ conceivable  set of rule:value pairs in CSS, even where it adds no value at all. So you end up using classes like `.block` rather than writing `display: block` and `.text-center` rather than `text-align: center`.

Of course you can mix Tailwind's more useful classes with regular CSS. But then you're breaking the Tailwind style-by-classes abstraction, and you have to maintain two seperate styling touchpoints for every element.

_"So what?"_ you might ask, what's wrong with just using those classes rather than CSS? It certainly saves some keystrokes. Here is where Tailwind introduces new problems it shouldn't have to solve in the first place.

### Reinventing CSS

Tailwind has to reinvent everything regular CSS can already do. Media queries, pseudo elements, selectors, and states. All of it now has to fit into the classes-only paradigm. 

Tailwind achieves this with what it calls modifiers. By prepending Tailwind classes with `md:` they will only apply above the `md` breakpoint. By appending `hover:` a class will be applied in a `:hover` state. And so on. 

Each of these tools is a poor facsimile of the functionality gaps it has to fill. Want an `:nth-child` or `~` sibling selector? Back to CSS. Want to target the devices between two breakpoints? Back to CSS. Want to target children of an element? Back to CSS. You get the picture. 

Of course you _can_ go back to CSS to do any of these things. Lovingly coined "bailwind", almost every project will need at least a little custom CSS when Taiwind's classes and modifiers just don't cut it. But then you're back at breaking the Tailwind abstraction, and giving yourself maintenance headaches.

And if this is already a given, then why use pointless classes like `block` when it adds no consistency or maintainability value over writing `display: block` in CSS, other than a few saved keystrokes? Because while gap-filling classes like this don't add value, they _do_ add a new Domain Specific Language (DSL) to learn on top of the CSS we all already know.

### Class soup

The thing every critic of Tailwind yells at first, its enormous class strings. Yes, they're ugly, but who cares. The problem isn't a surface-level developer perfectionism one. It again comes back to modifiers. 

Every rule that applies to a modified state needs its own class with its own modifier. Unlike in CSS where these states and pseudo elements are naturally organised into logical blocks, Tailwind's modified classes can very quickly become a huge, difficult to maintain mess that has to be carefully teased apart line by line.

Take a contrived example of the button we had in the intro of this article, with an icon of some sort added to a `::before` pseudo element, and less-than-ideal attention given to class ordering.

```html
<button class="relative before:absolute bg-blue-500 hover:bg-blue-700 text-white before:left-2 font-bold before:text-sm py-2 px-6 rounded  before:top-1/2 before:content-['\f00c']  before:-translate-y-1/2">
  Button with icon
</button>
```

Of course in this particular example the icon would be better placed as a real element inside the button, but the point stands. Without (and even with) careful ordering of classes, these jumbles very quickly become a maintenance nightmare. 

### JIT to the rescue?

Tailwind's new [Just In Time mode](https://tailwindcss.com/docs/just-in-time-mode) compiles just the classes you use on the fly, rather than pruning back a goliathan stylesheet after the fact. It allows you to use modifiers everywhere out of the box, and most importantly write arbitrary values right in Tailwind classes, like `margin-[100px]`. 

This is another language feature that was added to Tailwind's style-by-classes DSL in order to fix problems it introduced itself. And while arbitrary values mean you don't have to break out of Tailwind's paradigm as often, they also diminish the core value that Tailwind provides — a single source of truth for a whole project. Taken to its logical extreme Tailwind JIT is really just reinventing CSS, bit by bit.

## The Solution

As I said at the very beginning, Tailwinds' central idea is a very good one — a low-level, utility-driven design system to get rid of magic numbers and bring consistency to your CSS. The problem was the implementation.

Thankfully CSS now has the same solution as every other language to consistent values: variables. CSS variables, or more properly [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*), are fairly new to the language but already adopted by every major browser, and used extensively in Tailwind's own internals.

For example, Tailwind's `.p-4` padding utility could be rewritten like this

```css
:root {
--p-4: 16px;
}

.button {
  padding: var(--p-4);
}
```

And since we no longer have to write separate classes for every rule:value pair, we can greatly simplify our utility-first design system. We could have one set of size variables that can be applied to any part of padding, margin, width, height, position, etc. Without needing separate utilities for every combination of every property.

```css
:root {
  --size-2: 8px;
  --size-4: 16px;
}

.button {
  padding: var(--size-2) var(--size-4);
  margin: var(--size-4) 0;
}
```

And since variables are part of the platform, they have a native runtime. We can interact with CSS variables using Javascript, and update them dynamically. This makes things like reskinning a whole interface for dark mode possible with just a couple lines of code, without introducing any new utilities or tools.

```js
function enableDarkMode() {
  document.documentElement.style.setProperty(`--color-background`, `black`);
  document.documentElement.style.setProperty(`--color-text`, `white`);
}
```

So why don't we, instead of reinventing the styling paradigm altogether, just abstract all the values in an interface into a single source of truth by putting them in CSS variables that can be used _anywhere_, in regular old CSS, without all these new problems?

## By Example

{% github peppercornstudio/pollen %}

[Pollen](https://www.pollen.style) is a standards-driven CSS library that came from this line of thinking. It takes Tailwind’s core ideas and reimplements them as a 0.85kb collection of plain CSS variables. It can deliver all the key benefits of Tailwind without reinventing how we write CSS, thanks to the tools we already have in the web platform. Since it’s just plain CSS it can be used anywhere in any context and in any way.

{% codepen https://codepen.io/madeleineostoja/pen/LYjGjGa default-tab=css %}

### But you might not need it

Full disclosure: I wrote Pollen. But I'm not trying to sell you on adopting it. I'm trying to sell you on the ideas behind it for your own work. 

Pollen is just a collection of hopefully useful CSS variables, translated from Tailwind. But If you already have a solid design system with sizes, typesets, colours, and the other shared values of an interface, then you probably don't need Pollen, and you certainly don't need Tailwind. Write them in one place as CSS variables, and use them everywhere. That's the way out of this insanity. 

Bring consistency to CSS by getting rid of magic numbers with variables. The other problems of CSS (deep composition, leaky inheritance, performance optimisation) aren't solved by Tailwind, nor by CSS variables. But they are made harder by the new DSL Tailwind introduces. At least by sticking to regular CSS you have all the other patterns and tools we as a community have been working on for the last decade at your disposal, without any gotchas.