- 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
-
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 ↩