Jorge Manrubia

August 6, 2022

Utility-first CSS

maik-jonietz-_yMciiStJyY-unsplash.jpg


Recently I've found myself with the need to use a CSS toolkit a couple of times. The motivation was building from scratch some internal admin tools. I had heard great things about Tailwind, and David had bootstrapped a Rails integration for it, so we decided to go with it for building a new version of our internal Basecamp admin panel.

I was familiar with the utility-first CSS principles but had no experience with the paradigm of only using utility classes for everything. I was really curious about how Tailwind code would look. We got a license for the commercial components toolkit, and I remember being a bit shocked when I started browsing the snippets. For example, this is how a breadcrumb component looks in Tailwind:

<nav class="flex" aria-label="Breadcrumb">
  <ol class="inline-flex items-center space-x-1 md:space-x-3">
    <li class="inline-flex items-center">
      <a href="#" class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white">
        <svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"></path></svg>
        Home
      </a>
    </li>
    <li>
      <div class="flex items-center">
        <svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg>
        <a href="#" class="ml-1 text-sm font-medium text-gray-700 hover:text-gray-900 md:ml-2 dark:text-gray-400 dark:hover:text-white">Projects</a>
      </div>
    </li>
    <li aria-current="page">
      <div class="flex items-center">
        <svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg>
        <span class="ml-1 text-sm font-medium text-gray-500 md:ml-2 dark:text-gray-400">Flowbite</span>
      </div>
    </li>
  </ol>
</nav>

Was I meant to drop those chunks of unintelligible HTML in my code? Well, not directly: the recommendation is to abstract things out and create components, for example, using React or Vue. So we created an internal library of Rails helpers that look like this one:

def table(&block)
  tag.div class: "flex flex-col mt-5" do
    tag.div class: "-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8" do
      tag.div class: "py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8" do
        tag.div class: "shadow overflow-hidden border-b border-gray-200 sm:rounded-lg" do
          tag.table capture(&block), class: "min-w-full divide-y divide-gray-200"
        end
      end
    end
  end
end

It worked fine, but creating the library of helpers represented a good amount of work, and, at the end of the day, the ugly and hard-to-read code was still there, just hidden. I felt quite a bit of friction: this way of using HTML and CSS didn't fit with my mental model, and I had a hard time shaking that off. 

Reading a piece of HTML and quickly understanding what it does is a feature to me. I understand how you can achieve the same by building higher-level components in whatever view technology you use, but that's more work and a new layer of code to maintain.

In comparison, check how the breadcrumbs components look in Bulma, which uses a combination of semantic CSS classes and utility classes:

<nav class="breadcrumb" aria-label="breadcrumbs">
  <ul>
    <li><a href="#">Bulma</a></li>
    <li><a href="#">Documentation</a></li>
    <li><a href="#">Components</a></li>
    <li class="is-active"><a href="#" aria-current="page">Breadcrumb</a></li>
  </ul>
</nav>

That's something I can understand and use directly in my view templates without creating a component. Those are two problems removed from my plate. On that note, I was surprised to see this article by Netlify praising Tailwind readability. Code aesthetics is undoubtedly a subjective matter.

Of course, the utility-first paradigm exists for a reason. It's the result of many smart folks thinking about how to solve recurring CSS problems in large codebases. I recommend reading this article that explains the evolution from semantic CSS to utility classes, describing the problems inherent to each approach.

I understand the value of utility classes. We use them in our apps, and I have seen discussions about leveraging those more. Now, should you go all-in with that approach and not use semantic CSS at all? Since I'm not a CSS expert, I can't answer that with any degree of confidence, but I am highly skeptical. What I am certain of is that I don't love how the code looks and, thus, I have a hard time enjoying the paradigm.

I think this is one of those situations where a radical stance on a complex problem is very appealing: building UI components without writing CSS is a powerful stance. It removes the need to choose between fuzzy options at coding time and solves many CSS problems. But it also comes with taxes to pay. If anything, make sure you understand those because hot new tools and paradigms always look tradeoff-free at first.