April 7, 2021

Tailwind CSS: Hype and Hurdles

Tailwind CSS is a utility-first CSS framework that the Craft CMS community (and the web dev community at large) has been raving about for the last few years. In 2019, I finally decided to look into it and, like everyone else, was immediately horrified at the non-semantic HTML and apparent lack of separation of concerns. Years of CSS "best-practices" and writing BEM class names had convinced me that there was a correct way to be handling this sort of thing, and this certainly wasn't it.

Eventually, I was convinced by watching A Real-Life Journey Into the Opinionated World of “Utility-First” CSS with Simon Vrachliotis from Dot All 2018 and reading Tailwind creator Adam Wathan's post CSS Utility Classes and "Separation of Concerns". Both addressed real problems I struggled with on several real-world projects. I'd often find myself making seemingly harmless compromises like adding a .news__item class to a blog post to get the same styling. In isolation, this sort of thing isn't really a problem, but as a site gets further into its lifespan and more change requests come in, they have a tendency to layer on one another until your naming conventions become a barrier to understanding and effectively maintaining a project.

My first foray into trying out Tailwind came late in 2019, before the 1.2.0 release that added two features I really struggled without: transition and grid support. I felt like there was just one too many missing pieces at that point, so I bounced off it after one project and went back to my old BEM ways. However, I immediately found myself missing basic utilities like .text-center and .mb-0 and writing my own. As soon as I started sprinkling in a few utility classes I became frustrated because I needed responsive variants like .md:mb-0, but of course that was a somewhat complicated problem Tailwind had already solved, so if I was going to go down that road, I might as well just switch back to using Tailwind. 

The bottom line is: I wasn't sold on Tailwind when I first used it. It was only when I stopped using it and found myself missing it that I realized it really was a more effective solution to common front-end problems.

Eventually, I did return to Tailwind, and luckily the recent updates had added a couple new features that really improved the development experience. And that's the thing:

They just keep solving the problems I have with it

Almost every major release solves either a need I have, or just something I find inconvenient. These are just some examples in 2020 and early 2021:

  • 1.2.0 added transitions and grids
  • 1.3.0 added divide and space utilities for separating repeating elements
  • 1.4.0 added opacity options for backgrounds, borders, text, etc.
  • 1.5.0 added more variant options which helped with focus, hover and grouped elements 
  • 1.6.0 added animations
  • 1.7.0 added default line-heights, gradients, and made using @apply in custom CSS less painful since it could now do variants
  • 2.0.0 took a bunch of the big changes from 1.7.0 and pulled them out from behind the experimental flag and really improved focus style options
  • 2.1.0 added filters and JIT (just in time compilation), which drastically improves performance especially in dev mode

All that in basically a single year! The pace at which the Tailwind team improves the framework is nuts. Despite the constant improvements, there are still a few issues, so let's dive into those.

Some issues I still have with Tailwind

As a caveat before we get started with these, I should say I largely have only used Tailwind as it out of the box. I haven't really tried any plugins for Tailwind, and I'm sure there are some that solve some of these problems. I'm reluctant to build any big projects relying on Tailwind plugins primarily because of the fast pace of updates. I wouldn't want to get locked in to a plugin-based solution and then have to try to refactor everything when a native Tailwind solution arrives.

Styling content from a formatted text field

When working on a CMS-based website, you're often going to need to style big chunks of text from a WYSIWYG / Formatted Text field of some kind. Since you don't necessarily have the ability to inject classes directly into every paragraph tag coming from that field, you're going to have to improvise. Generally what I've been doing is wrapping the output in a div with a generic .formatted-copy class, and then using @apply to apply standard Tailwind classes to each standard tag type within that. Tailwind has since released a first-party Typography plugin that does basically the same thing with pre-baked typographical styles. This is fine, but you'll still often run into situations where you want to be able to override a specific text style and it can be a little frustrating fighting against the defaults you've established using @apply rules.

Styling based on ancestor classes or ARIA attributes

In CMS-based projects, I'll often style some minor things based on a body tag class — usually something like what post or entry type it is, what language the page is in, whether or not a modal is open, or what the color scheme for a page is. Similarly, it's nice to be able to style an accessible component like a dropdown or tabbed interface just based on ARIA attributes rather than classes. There's not really a way to do this with Tailwind — you're again going to be writing custom classes for this (well, a recent video discusses theming components, but I find the solution offered there to be a bit overly complex for most use cases). Now, there's nothing wrong with writing custom classes — Tailwind is utility-first CSS after all, not utility-only CSS — but it still feels a little counterintuitive to me and means you still have to look in multiple places to try to figure out why something is being styled a certain way, which somewhat undermines one of Tailwind's key benefits in my opinion.

Styling pseudo-elements

So, in my old BEM life, I would implement a lot of visual elements using pseudo-elements. Things like overlay gradients, angled corners, keylines under certain heading styles, etc. This isn't something you can do directly with Tailwind. You could write your own utilities or custom classes to style pseudo-elements, but if we're being honest there's a good chance you're just going to add more divs and use Tailwind classes to style them. This can leave you with a bit of "divitus" and in my mind is a bit more of a serious HTML pollution issue than elements having a bunch of classes.

Templating with dynamic class names

For Tailwind's built-in PurgeCSS to do its thing, full class names have to be present as a complete string in your templates or JavaScript. This means that you can't do something like these:

<div class="relative p-32 bg-{{ backgroundColor }}">Twig</div>
<div class="relative p-32 bg-<?php echo $background_color; ?>">PHP</div>

...and then pass in your color name as a property. This is something I'd commonly do if an author has color options for a block in the CMS, for example. Instead you have two options: either make a massive if statement at the top of your file where it assigns full class names based on the property, or just keep the template as is and make sure all your possible options are included in a comment somewhere in your project. I generally opt for the latter, because making my templates so much more verbose isn't really an appealing option. Still, it feels kinda bad.

Okay, so should you use Tailwind?

With those relatively minor quibbles out of the way, would I still recommend Tailwind? In general: yes. But in my mind, there are two primary considerations when it comes to deciding whether or not to use Tailwind on a new project:

First, what kind of project is it?

Tailwind is extremely good for component-based apps and sites. If you're building something in Vue or another front-end framework, Tailwind is a good match. If you're building a large website using more traditional methods, Tailwind is still a good match as long as you're building the site in a reusable, modular way. If you're building a flashy one-pager with extremely custom design requirements, you're probably going to be fighting with Tailwind more than it's helping.

Second, how is it being designed?

Are you both the designer and developer? Tailwind makes it super easy to design on the fly in the browser, and since you're made to stick to the config values you've chosen, consistency is enforced. There's almost always going to be a strong case for Tailwind in these situations.

Are the designers involved creating or working within a design system? Use Tailwind, but work closely with the designers so you're on the same page when it comes to things like color, spacing, and font options. Your Tailwind config file should mirror the design system as closely as possible. I would also strongly recommend in this situation ditching Tailwind's approach to spacing and type scales, and completely customizing those in the config file based on your project needs. By that I mean, instead of using the default p-4 for 24px padding and text-xl for 1.25rem (20px) text, standardize those to be p-24 and text-20. Using a pixel-based scale is much easier to work with since you're not having to remember what values mean or constantly refer back to the Tailwind docs. Also, when your designers surprise you by introducing a new font or spacing choice that falls between two values, you don't have to try to figure out what a word is for something that's between medium and large, or do something gross like having a class called p-3pt5. After all, we're trying to avoid naming things.

Are you working designers who aren't using a design system, are frequently inconsistent, or expect pixel-perfect accuracy? You might want to skip Tailwind in that case. As mentioned, Tailwind enforces consistency and having to make one-off exceptions for individual use cases really feels like it goes against the ethos of the whole framework.

Final thoughts

For the majority of my projects these days, Tailwind has become my go-to. It allows me to work much faster than ever before and write far less CSS. More importantly, I spend much less time worrying about whether or not my styling changes for one component are going to inadvertently affect something elsewhere on a project. Like any other approach, however, there are trade-offs, downsides, and adjustments to your workflow. 

If you're late to the Tailwind party, I'd strongly recommend watching Simon's talk to get pumped up about it, and then giving it a try. If you hate it at first, stick with it for a few days before throwing in the towel. You might find that after going back to your old ways, it's not long before you get the urge to sprinkle in just a few utility classes. Because you know what, they did kind of make a few things easier here and there didn't they?