Published on

Tools I Like: TailwindCSS

TailwindCSS has been a divisive topic in the web dev community, to say the least. The internet abounds with blog posts singing its praises, criticizing its every decision, and everywhere in between. Is my take on it so unique that it warrants yet another addition to this sea of opinions? Almost definitely not. But let's be honest: if you're reading this, it's likely because I've asked you to consider adopting Tailwind and I've shared this link directly with you, or it's because I've asked you to consider hiring me and you're skimming these posts to see what kind of developer you'd be hiring. Nonetheless, I'll try to find a unique perspective to use when I answer the question, "why do I prefer TailwindCSS for styling web applications?"

While preparing to write this post, I read a lot of other developers' positions on the topic, and I've found two things that the vast majority have in common. First, the line between TailwindCSS and the general "utility-first" paradigm is often fuzzy or nonexistent. This fuzziness is understandable because, for most of these developers, TailwindCSS is their first experience with utility-first CSS. But it certainly isn't the only incarnation, and it wasn't even the first one. So in this post, I'll try to separate the strengths and weaknesses of utility-first CSS in general from those of TailwindCSS in particular. The other thing that many of those posts—both critical and supportive—have in common is a lack of context around what alternatives they're comparing Tailwind to. Let's start with some background to provide that much-needed context in this post.

Background: My Experience with Styling Apps

All of the technologies or libraries I'll mention in this section are ones that I've used for real, live, production-level apps. This will not include things I've only experimented with or anything I've only used in a single, short-lived side project.

My experience with various styling technologies falls under three broad categories: traditional stylesheets, CSS-in-JS, and utility-based CSS1. With traditional stylesheets, I've used both vanilla CSS and LESS (and most comparisons I'll make here can apply to other pre-processor-based tech stacks like SCSS). I've also used several naming schemes for CSS classes, but I prefer BEM. The majority of my experience with CSS-in-JS is with styled-components. And with utility-based CSS, I started with tachyons before finding and switching to TailwindCSS.

Utility-Based CSS: Strengths and Weaknesses

A note on terminology: because I write this blog from the perspective of a React developer, I'll use the word "component" throughout this article to refer to a reusable—possibly configurable—markup structure. Feel free to substitute whatever the analog is in your tech stack of choice.

Strengths

Consistency via Design Tokens

The idea of design tokens was one of the original selling points for utility-based CSS. No more pixel-by-pixel nudging to make things align correctly. No more arbitrarily selecting font sizes in an attempt to match the design spec. No more "clever" color functions like fade or darken that result in dozens of different yet indistinguishable colors throughout your app or website. Design tokens give you a narrow set of pre-defined options, so there are fewer "correct" choices, and the size and spacing of elements naturally remain consistent because they all use the same scale.

No Context-Switching or Scrolling

With utility-based CSS, much like inline styles, the utility classes are applied right to an element. You can change an element's style by editing it directly, and the coupling between a component's structure and its styling is now explicit rather than implicit. You no longer need to maintain a mental map between two related concepts split between two files (i.e., structure and class names in one file and the associated styles in another file) or even between two regions of a page, like with CSS-in-JS.

Simpler Component Refactoring

When you need to move or rename a component with utility CSS, you just do it. You don't need to worry about keeping a second file in sync with the changes or renaming a collection of CSS-in-JS components. When duplicating a component, you don't need to worry about whether the existing class names (or component names) are still appropriate. And when you delete a component, you don't need to worry about leaving behind orphaned CSS rules.

Not Everything Needs a Name

It's a common refrain in software development that naming things is hard. We already need to name our components, so why should we punish ourselves by also trying to come up with a class for nearly every element within a component? This problem also plagues most CSS-in-JS solutions, where developers frequently resort to unhelpful names such as StyledSomething or SomethingWrapper. With utility CSS, the developers of those libraries have already done the hard work of coming up with the names (and we'll dive into how helpful those names are or aren't a bit later).

Weaknesses

Harder to Find Elements in the DOM

This problem is a side effect of "not everything needs a name." With traditional CSS classes—and even styled components, assuming you have the tooling configured correctly—it's reasonably straightforward to find any element in the browser's dev tools because most of them have some kind of name attached. With utility classes, they all start to look the same when you're trying to skim a large tree for the one element you're looking for.

This issue can also occur when trying to find an element at runtime. The lengthy class lists that are a product of the utility classes aren't very intuitive as query selectors, and the fact that they'll change with any visual changes makes them very brittle. Generally, I discourage using class selectors to target DOM elements, but many tools out there (like those targeted at marketing or product teams) frustratingly insist on using classes by default.

Hard to Predict How Classes will Override

This is mainly an issue when dynamically building a class list, like with the classnames library. A typical case I've run into is a card that we want to give a "selected" state with a different background or border color, but we can't use any built-in state modifiers like active.

import cx from 'classnames'

function Card({ title, body, selected, onClick }) {
  return (
    <div
      className={cx(
        // White background by default
        'bg-white border-gray-300 rounded-lg shadow-md p-4',
        // Blue background when selected
        selected && 'bg-blue-100 border-blue-200'
      )}
      onClick={onClick}
    >
      <h2 className="text-lg font-bold">{title}</h2>
      <p className="text-gray-600">{body}</p>
    </div>
  )
}

When selected is true, we apply both the bg-white and bg-blue-100 classes, but have no way to know which will win. Part of the problem is that we're still using the old vanilla CSS mindset, where we set all of the default properties and override those that need to change for a particular state. But with utility CSS, we don't know in what order the various bg-* classes are defined, so we don't know which one will win when we define more than one. Instead, we have to shift our mindset slightly to specify only the classes that apply to a given state and not rely on states to override each other.

import cx from 'classnames'

function Card({ title, body, selected, onClick }) {
  return (
    <div
      className={cx(
        'rounded-lg shadow-md p-4',
        selected ? 'bg-blue-100 border-blue-200' : 'bg-white border-gray-300'
      )}
      onClick={onClick}
    >
      <h2 className="text-lg font-bold">{title}</h2>
      <p className="text-gray-600">{body}</p>
    </div>
  )
}

Common Criticisms that I Disagree With

"It leads to ugly HTML"

This common complaint says that the long class lists make the markup ugly and harder to read. At first glance, this is true. But I've found that once you adjust to it (which doesn't take long), you learn to filter out the className attribute when it's not your current concern. Filtering like this becomes even easier with a formatting tool like Prettier, which keeps className attributes of any length to a single line. Compare that to similar solutions like the style prop for inline styles or the css prop standard in many CSS-in-JS libraries. Even worse (in my opinion) are libraries like ChakraUI, where you can find props for style, data, and behavior all mixed.

"It breaks the cascade"

I've usually seen developers with this complaint refer to the fact that you can't use nested selectors to customize how an element appears within different contexts, like having a heading appear one way when used as a page heading but another when used in a form. While partly true, this strikes me more as a reluctance to adopt the utility CSS mindset. With utility CSS, the idea is to be more explicit by applying those styles directly in that context. I consider this explicitness more of a strength than a weakness because explicit styles are easier to predict, easier to debug, and you can change an element's style in one context without breaking its styles in any other context.

"It's only good for prototyping"

Whenever I see this, I assume it means "I've only used this for prototyping." It's partly true in that it is valuable for prototyping. But I've had just as much success using it in production apps as in those prototypes. The only real difference is that using it in a bigger app may require more upfront configuration instead of simply installing it and getting to work. But that configuration sure pays off (more about that below).

TailwindCSS

Now that we've looked at the strengths and weaknesses of any utility-based CSS library, let's look at Tailwind specifically.

Strengths

More Than Just Design Tokens

Tailwind provides utility classes that do more than just specify design tokens and general layouts. For example, the "space between" utilities can add horizontal or vertical space between all children of an element, and the "divide" utilities can add a horizontal or vertical border between all children. You can also quickly create a grid with any number of evenly-sized rows or columns. Finally, the "group" utility helps you style nested children based on the root element's state (like changing the text color of a card's title when the user hovers over any part of the card).

Configurability

Tailwind is highly configurable and customizable. This configurability makes it great for adopting within an existing project that already uses a different styling library. For example, if the existing library already provides its own reset, you can disable Tailwind's preflight core plugin. You can also customize the color palettes, size/spacing scale, and font families. Not only does this help when adopting it with an existing project, but it also helps to avoid the "Bootstrap Problem," where one Bootstrap site looks like every other Bootstrap site.

Size/Spacing Scale is Abstract but Proportional

This one is a bit more subtle, but it turns out to be incredibly helpful. The scale used for height, width, margin, padding, space between, and others uses numbers to describe the relative size. By default, all of these properties use the same scale, and the scale is set up so that the numbers are proportional. That way, while you don't necessarily need to know exactly how big of a margin the m-2 class gives, you can know that it's exactly twice as big as m-1 and half as big as m-4. This proportional scale makes it painless to quickly try different values and hone in on the best choice.

Plugin System

Tailwind comes with a robust plugin system, so if there's a utility you need that isn't available by default, there's probably a plugin for it. And if there isn't, you can build one yourself. Plugins don't just add utilities, either. You can add components, custom base styles, and custom variants. The Tailwind developers also provide a collection of "official" plugins for things like form resets and styling prose text, the latter of which I use to style this very text.

You Aren't Limited to What They Give You

Tailwind provides a few ways to customize styles beyond what they provide out of the box. You can extend any of the core plugins' theme configurations to add a specific value or use arbitrary properties for any one-off styles. You can also jump into CSS whenever necessary and still reference the existing theme values. And because Tailwind uses PostCSS, you can also include any of their plugins, like nesting, to get syntactic sugar without needing to include a pre-processor.

Weaknesses

It's an Extra Build Step

Because it uses PostCSS (not to mention its own JIT compiler), it requires an additional build step of some kind, regardless of what build tool you use. This is evident from the number of different setup guides they include in their documentation. And although they offer a CDN-hosted version as an option, its use in production is strongly discouraged. The good news is that you probably won't need to worry about it again once you get it set up.

Common Criticisms I Disagree With

"Its Classes are Cryptic/Hard to Read"

Granted, they take a little getting used to. Especially if we're talking about writing code. For example, you'll likely find yourself using text-bold when you need font-bold or getting confused about why align-center isn't setting the align-items property correctly (hint: you want items-center instead). You'll need to keep the documentation close at hand while you figure out these quirks. However, in terms of reading the code, these mix-ups are less likely to happen. You can tell from reading it what the font-bold does and never need to question why it starts with font- instead of text-. Tailwind also uses very few abbreviations, and the ones it does use, like p- for padding or h- for height, are pretty obvious.

Conclusion

There are likely other ways to get all the benefits I've mentioned here. One solution I've seen come up a lot more recently is the open-props library, which has a similar philosophy but uses CSS custom properties instead of utility classes. You could even use some of these libraries to build your own ideal solution. But out of all the solutions I've used over the years, utility-based CSS lets me move the fastest with the fewest tradeoffs, and Tailwind's implementation gives me the right balance of utility and flexibility to feel like I can build anything.

Footnotes

  1. Note that the one technology I've used that doesn't fall neatly into one of these buckets is React Native's Stylesheet.create() API. However, my experience with that API consistently leaves me underwhelmed, so it won't warrant many comparisons in this article