Tailwind, and the death of web craftsmanship

There's a worrying trend in modern web development, where developers are throwing away decades of carefully wrought systems for a bit of perceived convenience. Tools such as Tailwind CSS seem to be spreading like wildfire, with very few people ever willing to acknowledge the regression they bring to our field. And I'm getting tired of it

History

Back in the 90s and early 00s, if you built a website, you probably didn't style it very much. If you did, you used tables, some attributes on the various HTML tags you had, and a lot of images. Changing anything required you to update every single occurrence of that thing within the HTML. There were attempts to work around these defects; people would build templating systems in Perl and other languages, but fundamentally, you were putting colors, dimensions, and such into your HTML. This was awful, and when CSS came out, even as limited as CSS 1.0 was, people eagerly adopted it. CSS+tables, some image slicing in Fireworks or Photoshop, and you could build a pretty good looking website. CSS 2 came out a few years later and dramatically improved things, allowing far better layouts to be achieved. In the middle of the 00s, Firefox and then Apple began pushing standards forwards, introducing many new features. Border-radiuses, gradients, box shadows, enhanced fonts, flexbox, grids, and many other layout tools. It was a veritable CSS renaissance. Pre-processor languages, such as Sass and Less, sprouted up, bringing useful bits of programming to CSS, and frameworks like Compass, Bourbon, and Neat grew out of them. Tools like Autoprefixer later cropped up, reducing the overhead of cross-browser support to almost nothing. Things matured, and we entered a period where building web apps was more fun than it had ever been.

The Cracks start showing

As we entered the twilight of this CSS renaissance, some issues CSS has begun to show up more and more frequently. Wrangling large CSS files became more and more tedious, deeply scoped selectors began causing issues, the fragility of a single global namespace pushed people towards componentization, towards building "website legos" to build with. There were attempts to tame the CSS beast. BEM, OOCSS, SMACSS, and friends pitched themselves as the "one true" solution. They all basically have something in common: they tell you to get rid of various CSS features to "simplify" things. Out of this rose Tailwind. Instead of writing your CSS, you just used a bunch of different utility classes to style things.

Atomic/Utility CSS

Often, when maintaining a large CSS codebase, we would write one-off utility classes. rounded to make something have border-radius, without having to rewrite it everywhere. Various small classes that adjust the padding. We always did this, so we could prevent changes from seeping out of the little place we needed them to everywhere else. We didn't use inline styles, because you weren't supposed to (and they make maintenance an unholy burden). Gradually, libraries of these utility classes began to grow. There were some that were passed around as gists, some that actually grew into things you could install via npm or a similar package manager (remember bower?), and some that were generated by or included in preexisting toolchains (Bootstrap sprouted some utility classes fairly early on).

The rise of tailwind

Tailwind started out as a particularly good set of Utility CSS classes. It was notable for being heavily configurable from day one. Its class names were reasonable, and it established certain useful conventions regarding sizing, color systems (very similar to that of Material Design), and lots of other common base settings. Some of these were "borrowed" from old libraries like Bootstrap, others were just created out of the need to buy-into utility CSS wholesale. Early versions of tailwind were horrifically heavy and slow. You'd have to ship megabytes of CSS, for a page that might have a half dozen styled "things" on it. And it was rightly lambasted for this. Utility classes were supposed to make things easier, faster, more convenient, and shipping a JPG worth of unused CSS was not in line with that. Tailwind eventually fixed this, with a generator approach, which would scan your codebase, pull out tailwind classes, and only put them in the generated CSS output. This also let tailwind grow the ability to have arbitrary values, without having to update a configuration file. Now you could do bg-[#ffccff] for a pinkish background, without having to add it to your color scheme. Useful, but dangerous too. Tailwind even sprouted component libraries, built atop tailwind. The tailwind devs have one, called TailwindUI, and there's an open-source one called Daisy.

The problem

The problem I have with tailwind is that it reduces your HTML to a gigantic pile of near-gibberish CSS classes. What would be a few, maybe even half dozen, lines of CSS in a separate file, are now shoved into the generated code, and repeated everywhere. Any structure to the styling of an item is completely gone; you have hover styles and focus styles and dark styles and whatnot all mixed together in a single big space-separated string. Finding CSS one-offs is not even feasible anymore, everything is a CSS one-off. Tooling is utterly broken by tailwind, despite the claims that there is good tooling on tailwind's own UI. Look at this screenshot of a web inspector on the Netlify admin dashboard, using tailwind:

"an element with a few hundred CSS tailwind classes"

Yeah, you might have auto-completion in your editor, but the browser inspector is utterly neutered. You can't use the applied styles checkbox, because you'll wind up disabling a style for every usage of that tailwind class. You have to manually edit the class attribute to remove classes to try and push your element to look how you want.1 Due to the tailwind compiler, which was a solution to the "shipping massive amounts of CSS" problem, you don't have a guarantee that the class you're trying to poke into the inspector view will actually exist.

You can't chain selectors. If you want your hover, focus, and active classes to be the same, you have to write them all. You can't do this:

.foo:is(:hover, :focus, :active) {
  background: purple;
  border: 1px solid blue;
}

You have to do this:

<div class="hover:bg-purple active:bg-purple focus:bg-purple hover:border active:border focus:border hover:border-blue focus:border-blue active:border-blue"

It gets even worse with dark mode and other variants.

It's all meaningless

In tailwind, it's common to write things like m-3 for a margin. But what margin? How big is an m-3? Trick question. It depends entirely on your configuration. An m-3 on my site and an m-3 on someone else's could be entirely different. Same goes for basically every other number in tailwind. They have absolutely no meaning, and there's no consistency. When writing it at work, I frequently either find myself opening up our tailwind config and the default tailwind config, just to find which size I want, realizing it's not there, and ultimately going with a m-[8px] or something similar, creating another one-off. And its not just numbers. Class names themselves are inconsistent. Generally a tailwind class will loosely reflect the underlying CSS. But then you run into the mess of justify and align. There are 4 tailwind classes that all impact justification and alignment. justify-*, align-*, items-*, and content-*. There's a bit of false consistency, both justify-* and align-* map to justify-content and align-content properties. But what about content-* or items-*? What the hell do they mean? It makes sense if you use it, but it doesn't if you try to explain it.

With "real" CSS, properties might not have the most explicit meaning. But that meaning is part of a standard, and you can look it up online. Tailwind has no requirements to not change the meaning out from under you. You have no guarantee that they're not going to "fix" the weirdness around the content/justify/align properties tomorrow, and now you've gotta update your entire codebase.

It throws out a ton of good stuff

Tailwind, and other utility frameworks, throw out the best and most often misunderstood aspects of CSS. CSS was explicitly designed with certain features, that, while complicated, are immensely useful once you can master them. The cascade, selector chaining (see above), and specificity are all very powerful tools. If you misunderstand them, yeah you can cut yourself. Just like if you don't use a saw properly you can cut yourself.

The cascade is probably the most powerful part of CSS, that gets completely tossed by tailwind. In short, the cascade allows you to have multiple stylesheets, multiple styles, that apply to an item, and at render time they are reduced down to a single set of applied styles. This lets you do things like have a general purpose stylesheet, and then theme or page specific ones that change parts of this stylesheet. You might protest that you've never used that, and I'd call you a liar. The browsers provide a set of base styles, and then we, as developers, add our own on top of them. We might use a reset style, which normalizes browsers base styles (less common these days, as browsers have done a good job of normalizing their styles to each other). You can also use the cascade to easily apply dark mode, light mode, high contrast mode, or other color schemes. You just have to write your original CSS in a judicious manner.

Selector chaining, particularly with the advent of new CSS selectors like is and where, allows very clean reuse of common bits of CSS. You get to separate the properties that affect how something looks from what is being affected. It's obvious, but in tailwind, you don't get that.

Finally, specificity is useful for how to resolve conflicts, what to do where two selector sets both match the same elements. Its complex and has bitten many developers hands over the years, but it can be a powerful tool when you need it. And 0-specificity selectors like where have arrived to help clean things up. And it's a lie to say you don't have to worry about specificity in tailwind; tailwind has its own specificity. Classes are read left-to-right by the browser, and so if you do b-1 bg-blue b-2, guess what? Your element gets both b-1 and b-2, and the browser will probably2 choose b-2 as your border, and throw away b-1

It's an obtuse abstraction

For all the simple stuff, tailwind is decent. Borders, colors, font sizes, all fairly trivial, and map well to utility classes. But then you start to get into "advanced" CSS stuff. Grids. Psuedo-elements. Gradients. All have very limited, if any, support in Tailwind. The built-in grid support is mostly just simple repeating grids. If you want different/better grids, you either have to use one-offs, or define them ahead-of-time in your tailwind configuration. Psuedo-elements have very limited support, and suffer massive amounts of repetition. Gradients give you simple linear gradients between a few color stops, but doing more complex gradients is just simply impossible, there's not even a reasonable one-off escape hatch. Tailwind does expose certain gradient values as CSS custom properties (CSS variables), so you can write your own CSS for gradients, but then you might as well ask "why not just write my own CSS in general"

@apply sucks

One of the bits of advice Tailwind gives is to use @apply and extract common blobs of CSS into common classes3. You'd use this like so:

.btn {
  @apply m-2 p-2 bg-blue text-white
}

Why?

Why bother to even use @apply? Just write the damn CSS. Extract color values and stuff to custom properties if you so desire, it makes maintainability easier, but don't do this. @apply is a gigantic code-smell, it goes against everything tailwind supposedly stands for, and doesn't make writing your CSS any easier.

It makes maintenanace a nightmare

Tailwind is rather far to the "write-only" side of the software maintenance spectrum. Once written, untangling which class in a big ball of classes causes an effect can be painful. This is never more apparent than trying to undo a decision made once in the past.

I recently had to update a site I was working on to support both light and dark color schemes. Tailwind has a nice dark: modifier built in, that converts whatever comes after it to a prefers-color-scheme: dark class. Problem is, the site I was working on was written, from the get-go, with a dark color scheme. In effect, I had to add light mode. And due to how browsers handle color schemes4, tailwind offers no light modifier (nor should they). My process of adding the light mode was thus reduced to a painful series of find all instances of a certain color, evaluate them, and if they needed, add the light color as the default and a dark: modifier to their expression. It wasn't as simple as a global find-and-replace, as there were some places we always wanted the dark colors to shine through.

Were this done using a more traditional CSS approach, I could just globally make the site use light mode, and then add a dark stylesheet that only affected the elements in question, or I could have updated color tables. Both are far easier than having to skulk through dozens of HTML templates, trying to find all the selectors that needed manipulation.

What's the alternative?

Scoped CSS and component based design. These are basically a magic bullet for any and all valid complaints about CSS.

Scoped CSS lets you write CSS how you want, without having to worry about its impact outside the local environment, and components help you define said local environment.

Component driven design is wonderful; you just write the base components, and then build things using those components, using other components, and everything just slots together. React, Vue, Svelte, Surface-UI, and many other tools all espouse this paradigm, and if you haven't tried them, you really should.

For example, here's a simple button in vue:

<template>
  <div class="button">Some Button</div>
</template>

<style scoped>
  .button {
    margin: 5px;
    padding: 5px;
    color: white;
    background: blue;
  }
</style>

I don't have to worry if some other component uses a .button class, as my styles are scoped only to the button in this component. And using that button elsewhere in my codebase is trivial, I just write <Button />. No need to copy around a ton of tailwind styles, no need to extract things into global .button classes, none of that. I just have a button, and it works. The end.

Overrides are easy enough, you just add props to your component, that let you tweak predefined values about said property. You can add a class prop, and pass in arbitrary classes, and then parent components can affect child components via their own locally scoped styles.

Common defenses of tailwind that I've heard

Tailwind is faster to write

Not in my experience, and ultimately, who cares? I've never found the bottleneck of developing code to be having to write margin instead of m, and I have emmet anyway, so I just do m10, hit tab, and get margin: 10px. You might have a wee bit of credibility in that I don't have to come up with semantic names for everything, but that's solved by using scoped styles and components.

Tailwind isn't actually bloated, it compiles the classes you need only for prod

The CSS tailwind generates might not be bloated, but repeating the gigantic strings of classes all over your codebase certainly adds to the size of the final HTML output. And yes, things like gzip will reduce the network transfer size of said files, there's still the parsing complexity. Browsers are good at parsing HTML, but it still takes time and CPU cycles.

Tailwind helps you stay consistent

Unless you use one-offs. Or use different "numbers" when writing your margins and paddings.

Tailwind is better than inline styles

Yeah, it is, but barely. That's like saying "this apple is slightly less rotten than that one." Both are rotten

The proper way to use tailwind is to make your own utility classes via @apply

When you're reaching for apply, why not just reach a little further and write real CSS?

Tailwind's configuration lets you define values ahead of time and then reuse them everywhere!

I've had value tables in Sass since 2008. Juniors still come along and just do margin: 13px. In tailwind, they do m-[13px]. No difference. At least with CSS its centralized.

The death of craftsmanship

Tailwind is a symptom of what I feel to be a larger problem in development. There's been a rapid deterioration in pride-of-craftsmanship in development. It's naive to believe that "back in the old day" everyone wrote everything with a perfect eye towards beautiful craftsmanship, and now we just push code out as fast as we can. I remember having to use ugly CSS hacks to get IE to render things properly, or float clears to get containers not to shrink under an inline image. But there was definitely more interest in "doing it right" rather than dismissing it as a problem that wasn't worth solving.

I don't want to dismiss tailwind as "for juniors" or as "for backend engineers forced to do frontend," but there is a kernel of truth to those statements. The people I've seen who are most excited over tailwind are generally those that would view frontend as something they have to do, not something they want to do. Juniors are still learning, and so they're attracted to the marketing spiel of tailwind, and see "look how easy it is I have to type less!" They don't have the experience to tell pyrite apart from gold. And backend engineers have an unfortunate tendency to view frontend as "not real engineering," as something beneath them. They used to reach for Bootstrap, now they reach for Tailwind. They want to get something passable done, and go back to "real" engineering on the backend.

That's not to say there aren't great minds that love tailwind. Many great developers that I have a ton of respect for use and espouse tailwind. I don't understand why, but they seem to have blinders for all of its problems, but maybe they see something I don't.

Maybe Tailwind is a symptom, an inflammatory reaction to the sorry state of CSS at the time it was conceived. Browser makers have kicked the can on native scoped styles again and again, and issues arising from specificity have persisted until very recently (and only are "gone" if you know how to use :where). I've seen other engineers, of all levels, stuck in a mire of bad CSS, and so to them maybe Tailwind seems like a lifesaver. But CSS is better now. It's not perfect, but it's better than its ever been, and it's better than tailwind. Give it another try. Don't reach for big globs of libraries to paper over the issues you think it has. Start off with something small and light. Use sass and autoprefixer, but not much else. Keep things simple and small. You might find you're even having fun writing CSS.

And it's not just frontend stuff that has seen this death. I've met senior engineers who don't understand git, and don't want to understand git. You point them at git-from-the-bottom-up, and they recoil as if it was a venomous snake. I've seen people, lead and principal engineers, who refuse to learn modern JS, insisting that since it was bad in 2006 its bad today. Worse still is some of these people have used their leadership positions to prevent the use of modern JS, of component frameworks like react/vue, from being used across an organization.

Addendum

Everything I wrote about here is mostly a problem I noticed regarding Tailwind. But other people have noticed them too. Here's some other things, some dated, some not, that expand on some issues of Tailwind:

Updates

  1. The Chrome inspector has apparently grown a nice little .cls button in the Styles tab, which lets you toggle on and off the classes applied to the selected element via checklists. Thanks to swanson on HackerNews for pointing that out!

  2. I say probably because it's very loosely defined behavior. What happens if you document is written in an rtl language?

  3. Except the creator of tailwind himself regrets adding @apply

  4. Light mode is the default, as far as browsers are concerned. If no dark style is provided, they will always fall back to light mode

The article “Tailwind, and the death of web craftsmanship” was written on and last updated on