Johan wrote this
@ 2024-02-20

I'm not holding it wrong, YOU'RE holding it wrong

It seems to be open season on Tailwind CSS! Go and read this post by Tero Piirainen. Nothing I haven’t heard before, it hits basically the same notes as the other stop-using-utility-css posts: it implies that people mostly use Tailwind because of successful PR, and it assumes that its proponents are React-pilled newbies who just haven’t been exposed to the beautiful fire stolen from the gods by Jeffrey Zeldman, Prometheus incarnate, in the early aughts. This one goes a step further and includes links for learning CSS! Because you are obviously a lost child in need of guidance! Let me condescend right back, because that’s what I do best.

On cleanliness

We see the adjective clean used nine times and the main thrust of the article is an exhortation to learn how to write Clean Code without using a framework. But it never really goes into what that’s supposed to mean. From the rest of the article we can infer that “clean” to the author means “terse” or perhaps “easy to skim” because he isn’t really arguing for anything, just against piling many selectors onto elements. I think that’s a terrible definition of clean. Maybe I’ll come off as a hypocrite here because I usually argue that good code means readable code. It’s a big part of why I enjoy working in dynamic languages. I don’t like adding keywords and types and squigglies to my code in order to make it more palatable to a computer. That always felt to me like the job of a compiler, not a human—my code is an artifact created for myself and others, something that is supposed to be read and parsed and modified by people. There’s a lot of room for argument here, of course! What’s readable, after all? People have very different opinions on that. (Ask JK Rowling or James Joyce.) To me, an element called <span class="block font-bold align-right text-xl"> tells me a lot more than an element called <span class="theme__table-row-count">. That all has to be prefaced with “within reason” because both utility-first class names and regular CSS selectors can really balloon in size and footprint, confusing everyone. But in essence: the macro level certainly looks a lot chattier with atomic CSS but when you get down to the real actual day job of understanding and modifying code, Tailwind comes with a bunch of advantages that are hard to ignore. From reading Tero’s article (and later, Heydon’s article) I get the sense that “this looks too busy” is, in fact, nine-tenths of the argument. The rest is probably down to environment and prior experience. What types of sites have you worked on, how big were the teams, that sort of thing. After all, these CSS rules have to be applied somehow, and we’re basically just arguing whether to put them directly on the element, or in another file! Seems unimportant, perhaps, but tends to change dynamics in ways that matter more than one might think.

Some cherry-picked examples

While this is all interesting and fun in the abstract, it quickly devolves into mud-flinging about aesthetics. Heydon’s post is more interesting because it gives an example of what editor output might look like with a utility CSS framework, and he posits that it would look like this:

<p class="font-sans text-base leading-6 my-16">on</p>
<p class="font-sans text-base leading-6 my-16">and</p>
<p class="font-sans text-base leading-6 my-16">on</p>
<p class="font-sans text-base leading-6 my-16">and</p>

No sane person would do that, of course! I have this type of WYSIWYG style content in a few parts of our app, and this is how you’d solve that particular issue:

@layer components {
  .text-content p { @apply leading-6 my-16; }

Yes, the documentation tells you not to use @apply just to make things look “cleaner.” They don’t say “never use it” because this sort of generated text block is a great place for that technique. Unless you go completely hog with @apply it’s a great escape hatch when you need to reach for things lower down on the abstraction ladder. It even takes standard-ass CSS, you’re not limited to Tailwind’s pre-baked stuff.

Another interesting thing is what Heydon proposes instead: a global p style, and sibling selectors. And that would absolutely not fly in basically any of the apps I’ve worked on! Having only one type of p tag that looks the same across an entire site sounds… implausible? I would certainly assume it’s possible: maybe it’s a blog, maybe it’s an austere and disciplined newspaper, or a storefront that only has a certain type of listing? I don’t know. But I do know that I have a plethora of different p tags on our site, doing many different jobs. Same with our spans, our h2s, etc. That’s the “environment and prior experience” stuff I was talking about earlier. Having to name, style-reset, and override every deviation from a standard element is exactly what we’re trying to get away from, and we do it by flattening the cascade and using multiple classes. If that’s not something you’ve had to face, I completely understand how Tailwind looks stupid to you.

The Catalyst stuff that Tero rails against seems similarly cherry-picked. Those are library components built to work for everyone and take a ton of user customization. They can’t just apply a couple of styles and be done with it, they have to take lots of things into account. If you wanted a no-nonsense black button like the Catalyst one without any cruft, that would probably look a lot more like this:

<button class="bg-black text-white font-bold border-zinc-900 py-1 px-4 rounded-lg transition-colors hover:bg-zinc-700">

Which is wordy, yes, but not nearly as complicated as the articles make it up to be. Plus, you can see exactly what it does on the tin. In the end (and as I argued in my last post) you’re likely only writing that unwieldy chunk once and then it lives in that component until the day you need to fiddle with it again, which could take years. I want to show what things might look like in a Tailwind codebase and instead of arguing theoretical points like the rest of the peanut gallery, I’m going to bare my ass and show you some real, actual code. Here’s one of my views—I picked one that was small and focused but still showcases some of the interesting properties. (I also didn’t pick the worst one, because I don’t want to look bad in front of the entire Internet.) Anyway:

module Themes
  class Show < ApplicationView
    include UI::Buttons
    include UI::Containers
    include UI::Headers

    def initialize(theme:, reviews:, auctions:)
      @theme = theme
      @reviews = reviews
      @auctions = auctions

    def template
      div(class: theme_class) do
        container do
          span(class: "text-jaffa") { t(".collection") }
          h1(class: "text-4xl font-medium tracking-tight max-w-lg") { }
          p(class: "max-w-2xl text-lg mb-7") { @theme.blog_post.excerpt }
          a(href: blog_post_path(@theme.blog_post), class: BUTTON_WHITE + %w[inline-block]) do
          h2(class: H2_MEDIUM + %w[text-white pt-20 mb-0]) { t(".all") }
        render @auctions)

      render @reviews)

    def video_embed
      return if @theme.video_embed.blank?

      container do
        div(class: "w-full relative pb-[56.25%] mb-8") do

    def theme_class
        %w[bg-black text-white],
        -> { @theme.video_embed.blank? } => "pt-28"

You can go ahead and shame and pick apart my code if you like but even so, I’m gonna go right ahead and say that this view fits my idea of “clean” even if it doesn’t limit itself to one class per element. The first thing we notice: this is written with Phlex in Ruby. If I didn’t have this kind of power in my components and views, I’d probably be less of a Tailwind fan! I wrote a bunch about that in my previous rant but the general idea is that atomic CSS really only shines in a setting where re-use is easy, and can be done in multiple ways. The things that are likely to be recycled are extracted to components ( or mixins (container do..end) that I set up as I go, and then there’s a whole bunch of that random willy-nilly hurdy-gurdy markup that inevitably crops up in every view you make. That apparently doesn’t happen to Tero or Heydon, so maybe I’m doing something wrong! At any rate, one of my least favorite questions is “what do you name the paragraph element that contains the excerpt?” BEM would probably call it theme-view__post-excerpt or something. My answer here, though, is a satisfying max-w-2xl text-lg mb-7.

Because I just DO NOT CARE about that element.

This is the only place where it exists! It’s definitely not cool enough to be a component and I don’t even know that it’s cool enough to deserve its own CSS selector. I would resent having to come up with a name for it, and targeting it with a child selector would kill our ability to modify it with other selectors after the fact. If you’ve created and named that selector and it’s now cluttering up your stylesheet, I think you’re doing “clean” wrong: you’re not only wasting cycles coming up with nomenclature for it, you’re also creating a tightly-coupled artifact in a different place and likely duplicating a bunch of rules in the CSS you send to users. Then there’s the elephant in the room: most of our work as developers is reading and changing code, and the one-selector-per-element and default-element-styles strategies are not well suited to change.

Create Read Update Delete

If we have our selector and want to remove or edit that little element six months later, we have to go into detective mode. Is the selector safe to excise from our stylesheet? CTRL+Shift+F, I suppose. Is it used in another capacity somewhere else and we have to be careful when modifying it? CTRL+Shift+F again. And then we have to worry that maybe our global search didn’t find all the instances because the class could be programmatically created somewhere, or maybe the name is built from SASS nesting or something. And once we’ve changed it, we have to visually check every instance of the element because it’s entirely possible we’re being trolled by the cascade or a global default or some sibling selector we didn’t anticipate when changing it. If you’ve been doing CSS for a while then you’re probably very careful with those tools, the same way you’re probably very careful with things like ORM callbacks. They can be useful sometimes but they’re very sharp objects that carry a big potential for unintended consequences.

If we look at this from the other end, though: with Tailwind, what you see really is what you get. The only place that’s going to change is the place where you made the change, and it’s right there in front of you, on the element you’re working with. I can be confident that if I change something on the stupid p holding my excerpt, that’s exactly what’ll happen. No spooky action at a distance, as Einstein would have put it. People usually praise Tailwind for how fast it is to build something out, but it gets less credit for how robust your site becomes. I suppose that’s a double-edged sword, because if you really wanted every link changed from blue to hot pink globally, there’s a lot more search-and-replace. Completely true. But it’s predictable in a way that massaging the cascade isn’t: the worst that can happen is that some links stay blue, the way they were before. If I have to pick one of “parts of the site may be inconsistent” or “you have no idea what will break if you update your styling” I’ll pick the former, all day, every day.


I’ll just quickly touch on two other things that rubbed me the wrong way. The first is the trope with sweeping hand gestures about how far CSS has come in recent years, and yes, it has gained a lot of features, great features at that. The standards groups are doing a sterling job and things are very easy now compared to when we hacked tables in Netscape Navigator. But lack of features was never truly the problem when it came to wielding CSS more generally on a site and in a team. Maybe container queries and scoped styles will reveal the silver bullet and we’ll all happily link arms and dance into the sunset until AI takes our jobs and leaves us destitute? But so far none of the features have really hit the top level and tackled the difficulties inherent in designing a good and robust CSS framework that works for more than one person. CSS will keep improving and I’m sure Tailwind will keep incorporating the features that get released, but I’m using it because of the big picture: working at a low specificity and opting out of most of the cascade brings a lot of advantages to the kind of work I’m doing.

"Apu takes a bullet" meme with Apu flinging himself between Tailwind and criticism It’s me. I’m the weird nerd.

I also don’t like the general conflation of Tailwind and React. I keep seeing noses turned up on Mastodon about how the tech bros are ruining everything by using Tailwind. I also saw a truly epic take about how Tailwind was a reaction by cis white men to CSS being feminine-coded, which made me slap my forehead and groan loud enough that my kids came over and wondered what I was doing. If you know anything about me you know that I’m not a big fan of React and I’m not a big fan of tech bros either. I’ve spent my entire career far away from Silicon Valley, I’m basically religious about progressive enhancement, and I’ve been shaking my head at the lost decade for many years. Feel free to dislike Tailwind if you want! Smear the tech all you like. I would just like you to smear it on its own merits, not its associations.