How I use Tailwind CSS in 2024
Tailwind CSS is great. It’s an exceptionally well-crafted and thoughtful library that has been transformative for many thousands (millions?) of developers who have struggled with CSS or just wanted a different way to do it.
Over the past several years, I’ve used Tailwind CSS on countless projects across every degree of buy-in. I've built a massive web app styled exclusively with Tailwind's flagship utility classes. I've also used those classes for occasional inline modifications in projects with a traditional CSS stylesheet. I’ve seen the benefits and the pitfalls, and I’ve found a happy middle ground of how I can most effectively utilize it.
All of my issues with Tailwind CSS have been argued and countered ad nauseum by countless others, so I won’t waste any ink on the specifics. Ultimately, most of my friction with Tailwind CSS revolves around the mental separation of concerns between CSS and HTML.
Hypertext Markup Stylesheets
For me, there is a fundamental difference between the markup (HTML) and the styling (CSS). When I write CSS, I’m taking a set of style attributes, bundling them together and giving that bundle a name. For instance, this set of styles is a button, and its name is btn
:
.btn {
display: inline-flex;
border-radius: 4px;
background-color: var(--color-primary);
...
}
When I switch over to the corresponding HTML, I’m using those named bundles (i.e. CSS classes) to compose an interface. I’m not as concerned about the specific underlying styles, but rather the result of those styles. I need a styled button, so I create a <button class="btn">
.
In the real world, of course I’m writing HTML and CSS in tandem. I’m constantly tabbing back and forth between the two, but my mindset shifts subtly as I do. And as a project progresses, I’m typically writing less CSS and more HTML.
My mindset with CSS is similar to when I’m working in Javascript or PHP and I write a fairly complex function. I fully wrap my head around all the technicalities, build that function with great care and intentionality, write some automated tests, and make sure that it’s bulletproof. But once that code is committed, I can immediately forget the inner-workings of the function. Going forward, all I need to know is how to interface with it. I want to add user invites to a new part of the app? I don’t need to remember how exactly that functionality was implemented. I just call inviteUser(emailAddress)
. I’ve long forgotten the specific logic of what happens in there, but I’m confident that three-months-ago-me knew what I was doing.
Naturally, CSS requires a bit more constant tinkering during implementation, but you get the idea. After I’ve built the .btn
class, when I’m writing HTML I just want to be able to slap that class on an element and have it look like a button. At that point, I don’t care what the font-size
or border-radius
are specifically, just that they’re what they should be.
Tailwind collapses those two concerns into the HTML. Instead of using hand-crafted CSS classes to build a layout, I have to keep track of what each line of utility classes equates to, as well as how to structure the elements. Rather than seeing <div class="listing">
I see <div class="flex items-center justify-start flex-col gap-4 px-4...">
and my mind has to quickly parse through the classes every time I look at it. And even then, it’s not clear what it is. The cognitive overhead of writing HTML goes through the roof.
Tailwind’s the worst, huh?
Tailwind is great! I promise I like it! I just don’t like going all-in on it the Tailwind way - styling the majority of my interface via inline utility classes.
The Tailwind team has done an amazing job with one of programming’s most difficult problems: naming things. They’ve subtly renamed nearly every CSS declaration to phrases that are typically easier to understand than the underlying raw rules—or at least more succinct.
justify-between
is a great, scannable shorthand forjustify-content: space-between
.rounded-sm
is more concise and contextual thanborder-radius: .25rem
or evenborder-radius: var(--border-radius-sm)
.underline
is perfect alias fortext-decoration: underline
. I don’t care that it’s a text-decoration; I just want a line under the words.
Tailwind is also great for gently enforcing standard units. I can define the text sizes that my app uses, and only use those sizes. That way I don’t have 14px
text right next to 15px
text.
Going back to my set-it-and-forget-it tendencies, I can define all of my app’s sizes, colors, and breakpoints at the beginning of development and not need to worry about the specifics again. text-sm
is small text. I understand what that means much quicker than font-size: 0.875rem
.
I want my CSS to have all of that brevity and consistency, so I rely on Adam’s least favorite Tailwind feature: @apply
.
@apply
is a really, really nice and concise way to write CSS. For example, one of the most common sets of classes I write is:
{
display: flex;
align-items: center;
justify-content: space-between;
}
With Tailwind @apply
, that can be collapsed into:
{
@apply flex items-center justify-between;
}
At this point, you're probably thinking that I’ve simply moved the visual clutter of a single line of utility classes from the HTML to the CSS. Which, you know, fair point. But by moving them to the CSS, I can organize them however I want. I typically break @apply
declarations into multiple lines, grouped in a way that makes sense. For instance:
{
@apply flex items-center justify-between;
@apply px-4 py-6 my-1;
@apply text-sm font-bold bg-green text-white;
}
And most importantly, all of this is back where it belongs: in the CSS. I can still package up all of the styles I need into reusable classes while leveraging some of Tailwind’s innovations. And when I need or want to write raw CSS, I can still do that right alongside the @apply
rules.
Then, when I’m writing HTML I can use Tailwind’s utility classes for one-off adjustments, like adding additional margin with my-6
or changing a flex child’s alignment with self-center
. You know, stuff that utility classes have historically been used for.
One side note: when using utility classes with custom CSS, I highly recommend configuring Tailwind to use !important
for all its classes. Without that, most custom CSS will override a Tailwind class. For instance: a.link { @apply text-blue }
is more precise and will take precedence over a Tailwind-generated utility like .text-red { color: #ff0000 }
. So <a class="link text-red">
would still be blue. With the important option set, utility classes will supersede any other non !important
styles, no matter how specific. You can also solve this using @layer
, but !important
works just fine for my needs.
May the wind be ever at your tail
Let’s face it. CSS is ugly, garbled and messy. I totally understand how Tailwind appeals to those who don't like CSS. But I love CSS! For better or worse, it’s been a significant medium of expression for me for over 20 years. Still, I’m all for ways to improve on the DX of writing it, while keeping what I think makes CSS special.
So as it turns out, the thing that turns me off to Tailwind in HTML is exactly what makes me love it in CSS. It makes HTML more bloated and cumbersome, but somehow makes CSS more pretty, concise, scannable, and fun.