Tailwind CSS: Caught in the Crossfire of the Front-end Divide

Very few technologies in front-end development raise such a passionate divide as Tailwind CSS. From people madly in love with it claiming that “it fixes CSS” to detractors that discard it as “just abstracted inline styles”, It would seem that no month can go by on Tech Twitter without a flame war about Tailwind. 

However, I believe the arguments about Tailwind are rarely scoped to the framework itself, but part of much deeper discussions. Let’s go through how it really works, dismantle some of the arguments from both camps, and address the most important question: is Tailwind the future of CSS?

What is Tailwind CSS?

If we had to put it in a one-liner, we could say that Tailwind is a utility-first CSS framework, but I guess that won’t mean much if we don’t get into a little bit of CSS architecture first. 

Historically, one of the biggest challenges of CSS has been keeping things organized, especially as the team grows and more people work in the codebase.  Someone said that “the max number of people that can productively work on CSS is one”, and there’s quite a bit of truth behind that joke. If we don’t set clear guidelines, CSS can be particularly tricky to maintain. 

Several CSS architectures have been developed throughout the years to help address the issue. From the good ol’ Object Oriented CSS (OOCSS) to Block Element Modifier (BEM) and Scalable and Modular Architecture for CSS (SMACSS), numerous solutions have been proposed to keep our classes organized while embracing the power of semantics, the cascade, and the fundamental paradigm of separation of concerns. 

But as the web shifted towards a JavaScript-heavy modular architecture, we’ve seen other approaches emerge. In one way or another, these approaches seem to be more compatible with the modern web (or at least, with the people developing it): various flavors of CSS-In-JS, styled components, and Utility-First CSS. 

The Utility-First Approach and Tailwind 

The utility-first approach (also known as Atomic CSS or Functional CSS) aims to take the single-responsibility principle and apply it to CSS, allowing us to compose complex components from simple, single-responsibility CSS classes. Instead of assigning a class with a bunch of declarations to an element, we can explicitly declare how it looks, bit by bit. No more .sidebar__title--highlight, simply assign it a class for its font size, another for the font-weight, and so on. 

As the biggest utility-first CSS framework, Tailwind provides us with an enormous library of pre-built CSS classes that we can use to compose our styles. It allows us to create prototypes incredibly fast, not having to worry about naming conventions (or naming at all), making changes feel safer, and generally resulting in smaller CSS files.  

The utility-first approach and Tailwind, in particular, mean such a radical departure from traditional CSS architecture that it catches a lot of flak from some CSS developers and a lot of praise from others. Let’s get into some of the arguments of both camps and check exactly what claims stand. 

Is CSS Broken? 

One of the boldest claims from some Tailwind evangelists is that CSS is broken somehow, and that Tailwind fixes it. Despite how loud those people can be on Twitter, I believe nothing can be further from the truth. However, we need to understand where that claim comes from and what people mean when they say “CSS is broken” or “CSS is hard” in order to know exactly what we are really discussing when we talk about Tailwind. 

As CSS occupies a weird place between design and development, “CSS is hard” can mean extremely different things. For starters, it can mean “design is hard”, as in choosing colors, fonts, keeping a consistent vertical rhythm, etc. This is evidently true, as it is a completely alien skill set to most programmers. And it’s no wonder, as it even works on a completely different part of our brains: graphic design relies primarily on visual-spatial intelligence, while programming works primarily on a logical-mathematical basis (and I would add CSS is mostly verbal-linguistic, but that’s a topic for a whole other post).  

What Tailwind does here is provide pre-vetted choices that simply work.  Instead of worrying about which tones of red go well with each other, you simply declare .bg-red-100, .bg-red-400, etc. Instead of worrying about the exact pixels to keep a correct vertical rhythm for your font sizes, paddings, and margins, you simply declare .text-lg, .p-1, .m-1.  Need to tweak the padding? try .p-2, .p-3, and so forth.

By limiting our choices and providing sensible defaults, Tailwind allows non-designers to rapidly prototype visually pleasing components. 

“CSS is Hard” can also mean “Implementing said design into code is hard”, simply because of the declarative nature of CSS and the limited time allowed for learning it in most front-end courses. I sincerely believe that 99% of the recurring issues with CSS can be prevented by properly learning the cascade, formatting contexts, stacking contexts, and two or three extra key concepts of CSS theory. You’ll be far less likely to get stuck if you understand how things work under the hood. But most developers are not even aware of those, and apparently can’t be bothered to learn them properly. CSS is deceptively simple, and that’s its demise. 

This is probably one of the most controversial arguments for and against Tailwind. It will help people who “know CSS (but not really)” to get away with it, by pretty much avoiding the cascade, and getting around collapsing margins and such by simply resetting everything to zero through preflight, it’s a very opinionated set of base styles built on top of modern-normalize.

Last but not least, “CSS is hard” can also mean “making the architectural decisions to build CSS in a sustainable way is challenging”. And this is definitely true. We all have seen codebases where the CSS gets incredibly verbose due to devs just adding stuff on top and never removing anything, fearing it would break the styles for some part of the codebase they may not even be aware of. Not to mention, incredible specificity wars where people will even reach for hacks to prevent their styles from getting overwritten. 

Tailwind’s utility-first approach means that the CSS file won’t be getting bigger, and sets the specificity flat so no wars ensue. Adding styles will rarely add weight to the CSS files. If anything, only your HTML (in whatever form you’re generating it) can grow.

It’s Not “Just Abstracted Inline Styles” 

An evident downside of utility-first CSS is how dirty our HTML looks. I have yet to meet a developer whose first reaction to a Tailwind component hasn’t been one of disgust. Seriously, just look at the thing. It’s ugly. 

  <div class="mt-4 md:mt-0 md:ml-6">
    <div class="uppercase tracking-wide text-sm text-indigo-600 font-bold">Why work with Scalable Path?</div>
    <a href="#" class="block mt-1 text-lg leading-tight font-semibold text-gray-900 hover:underline">Building teams is our business</a>
    <p class="mt-2 text-gray-600">Our personalized, step-by-step approach, powerful hiring platform and network of pre-qualified talent accelerates the hiring process and stacks the odds in your favor.</p>
  </div>

But hey, it works for many teams all around the globe, so there’s definitely merit behind it if you can look past that. Apparently, some people can’t, and they make the equally bold claim that “it’s just abstracted inline styles”. While I believe there’s some truth to that statement, it differs from inline styling in a million ways: 

  • You can’t apply inline styles to pseudo-elements or pseudo-classes. Utility classes can, and there’s plenty in Tailwind to handle your hover styles and everything you can possibly need. 
  • Inline styles won’t cache. And this is a huge hit for performance. Normally the browser will keep your stylesheets in cache, meaning it can access and apply any style much faster, whether that’s traditional or utility CSS. Inline styles are shipped as part of your markup, so they won’t cache and have to be processed every time. 
  • Single responsibility doesn’t mean single declaration. Unlike inline styles that declare properties and values one by one, utility classes are not necessarily limited to one declaration, but to one function. The best example of this can be found in the clearfix hack, which is probably the most widespread utility class ever. It’s also one that we’ve used even before that name was coined, let alone proposed as an architecture. In the clearfix hack we have a single responsibility – to fix the issues derived from using floats for layouts, but that’s achieved by three declarations
.clearfix:after {
  content: '';
  display: table;
  clear: both;
}

If you’re interested in further exploring the differences between inline styles and utility-first, I’d recommend Sara Dayan’s article on it.  

But again, I believe this is misinterpreting what people making this claim really mean: the utility-first approach means you’ll compose the look and feel directly in your markup (and have to learn a whole new set of words in order to do so). And to that, the other camp would reply: that’s exactly the point. To be fair, most of the class names are fairly intuitive and can be learned in just a couple of hours, and their documentation is exceptionally complete and well-written.

The discussion, therefore, goes back to the start. It’s not about inline styles, it’s not about Tailwind itself, or about the utility-first approach. It’s about whether or not we should embrace the most fundamental design feature of CSS, the one in the name itself: the cascade. 

Tailwind CSS File Sizes and Performance

We’ve all seen amazing claims of size reduction after refactoring old codebases from traditional CSS into Tailwind.  I’ve seen cases where they went from a dozen CSS files that weigh a mind-blowing total of 60Mb to a single file weighing a few Kb. 

This is not a small difference, in many cases, we’re talking about orders of magnitude. But I’d have to ask how you managed to let your CSS files get so out of control in the first place. If you’re shipping 60Mb of CSS, there’s something pathologically bad with your architecture and even with your team’s culture. Most claims of amazing size reduction after refactoring come from messy codebases where any refactoring would have achieved the same.

In all honesty, refactoring with Tailwind can sometimes feel like sweeping dirt under the rug. Your CSS files get incredibly clean, but all that dirt goes directly to your markup. So it’s mostly a pick your poison kind of situation. Your CSS files may be extremely small, but your markup is getting heavier, and unlike CSS, markup won’t easily cache. You’re saving a lot of repetition in your CSS, but then again, if you know how Gzip or Brotli works, you’d know that repetition is probably not the cause for big bundle sizes. 

In my personal experience, I’ve found that utility-first normally results in overall lighter bundles, but the difference is usually so small it shouldn’t be a primary factor in deciding whether to choose this platform or not.

No, You’re Not Shipping Mbs of Unused Code

Since we’re talking file sizes, let’s address more of the misinformed claims: that you’re shipping lots of unused classes. The development build of Tailwind is so extensive that it weighs a whopping 2.4Mb uncompressed (and 190kb when compressed with Gzip).   

But if you think these unused styles will be reaching your users, you’re in for a surprise. Tailwind integrates tree shaking through PurgeCSS, and setting it up is incredibly easy. You just have to enable purge in your tailwind.config.js file and it will work its magic at build time. 

// tailwind.config.js
module.exports = {
  purge: [
    './src/**/*.html',
    './src/**/*.vue',
    './src/**/*.jsx',
  ],
/*...*/
}

PurgeCSS will parse your markup and use regular expressions to find the classes you’ve used and compile those to a new file when the NODE_ENV is set to production. A stylesheet built with Tailwind only has its reset and the classes you’ve actually used in your markup, and rarely exceeds a few Kb. 

Furthermore, the most recent release includes a JIT (just in time) watcher/compiler that keeps an eye on your markup and automatically adds the classes you’re declaring to your custom build. This means incredibly fast build times and a great developer experience.

Modularity, Reusability and Maintainability in Tailwind CSS

One of the big promises of Tailwind is modularity, reusability, and maintainability. But those claims are also strongly disputed. Let’s dive in and try to clarify these claims and their pros and cons.

For modularity, there’s no denying that most devs value having everything “right there” instead of having to sort through multiple files to check what a given CSS class is actually doing.  But this can also be achieved by Vue / Svelte single file components, styled components, CSS-in-JS, and other strategies too. And even when using traditional CSS, there’s plenty of plugins for VSCode that let you check what a class does without ever leaving your markup.  I believe this feature to be highly overrated, and sometimes even counterproductive. If you let your class list get too big (which you probably shouldn’t, more on that later on), it can interfere with readability not only of your markup but your code overall. 

What stays true is that not relying on the cascade allows any element to be copy-pasted whenever you want without worrying that it’d break due to different scopes. But why are you copy-pasting so much, really? 

As for maintainability, we already covered how it stops your CSS files from growing and gives you peace of mind knowing that you won’t accidentally ruin some obscure corner of the codebase if you modify an existing class or – god forbid – decide to delete a rule.  So let’s focus on another thing that Tailwind does brilliantly well: theming. 
I couldn’t believe my eyes when I read people claiming “what if I used a red class throughout my app and the design team decides to rebrand it to blue?”. To that, I’d say that text editors have had a search and replace function available since the dawn of time, but there’s not even a need to go there. If for whatever reason you want to re-define what “red” means, you can do so in your tailwind.config.js file.

module.exports = {
  theme: {
    colors: {
      red: '#de3618',
    }
  }
}

But that’s not how you’d actually use it most of the time either. Most likely you’d want a primary color, secondary, and such, for ease of maintenance, so there’s no chance that “red” ends up meaning actually blue after a re-brand. Luckily, you can extend the color palette just as easily.

module.exports = {
  theme: {
    extend: {
      colors: {
        'primary': '#243c5a',
      }
    }
  }
}

This will automatically generate all the classes with the new keyword, for instance, bg-primary for your backgrounds, text-primary for your fonts, and border-primary for your borders. 

Of course, this doesn’t only apply to colors, but to their variations and any other properties you want to redefine. Tailwind configuration is incredibly powerful and robust, so theming in a sustainable way gets incredibly easy. 

All in all, maintainability seems to be on Tailwind’s side, but you shouldn’t lose sight of the fact that some changes can be harder in utility-first than in traditional CSS either. Say you want to remove rounded corners because the trend says they’re outdated… in traditional CSS that would mean changing a single class, in utility probably means deleting it from a gazillion different places or re-defining the rounded class to have no round corners. I’m sure you can see how that one could come back to bite you. 

The Debugging of Tailwind CSS Components vs Traditional CSS 

One of the most baffling arguments on the pro-Tailwind side is that it “makes debugging simple”. And yeah, it’s true that you don’t have to worry much about specificity and such, anything that’s affecting the style of an element is declared directly in its class attribute. But what boggles my mind is how anyone can say that a mess like this 

<button class="hover:bg-light-blue-200 hover:text-light-blue-800 group flex items-center rounded-md bg-light-blue-100 text-light-blue-600 text-sm font-medium px-4 py-2">

is somehow more readable than the organized, explained, and modifiable experience provided by your dev tools that are just a couple of clicks away. 

And this is evident whenever you start using Tailwind in production. Most teams will set rules on how many classes are enough, and compose abstractions if above that limit.  This can be achieved by extracting them to a variable (like Netlify did when they refactored to Tailwind) and carefully appending it to the DOM, or by using Tailwind’s @apply directive. 

For instance:

<button class="btn-blue">
  Button
</button>

<style>
.btn-blue {
  @apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
}
.btn-blue:hover {
  @apply bg-blue-700;
}
</style>

Looks promising, right? It also looks an awful lot like… traditional CSS. So now you not only have abstracted inline styles, but abstracted inline styles abstracted into traditional CSS. Abstractception

Ironically enough, this also goes against most of what the framework promised in the first place. We’re back to thinking names, back to thinking about specificity (somehow avoidable with @layer, but still), and not “having your styles right there” anymore.

Call me a skeptic, but I can’t get on board with the “easier to debug” argument when you lose track of where the styles are coming from. 

The Biggest Challenges for Tailwind 

Debugging long class lists is but one of the many limitations of Tailwind.  Advanced CSS features such as blend modes can get particularly tricky, and get us back to dealing with the cascade. Some parts of CSS require us to know the theory, isolating an element and their respective interactions is a challenge that we simply can’t escape.  

Anything requiring a path or advanced functions is another no-go. Animations can’t really be utility-first, especially if you try to set up chained execution (something challenging in CSS itself) or things dealing with clip-path, motion-path, etc

Another tricky part of the utility-first approach is dealing with conditional CSS, i.e. media queries. Tailwind does a brilliant job of setting up multiple breakpoints and providing us control of properties through a very clever use of prefixes. 

Want your element to change color on the medium viewports? simply use md:bg-red-400. Turn it blue at extra-large ones? xl:bg-blue-400.  It even allows us to set custom breakpoints and their names in the configuration, and automatically generate prefixed versions for all of its classes. 

For instance

// tailwind.config.js
module.exports = {
  theme: {
    screens: {
      'tablet': '640px', // => @media (min-width: 640px) { ... }
      'laptop': '1024px', // => @media (min-width: 1024px) { ... }
      'desktop': '1280px', // => @media (min-width: 1280px) { ... }
    },
  }

will automatically generate classes with the tablet:, laptop: and desktop: prefixes. 

But if dealing with basic styling sometimes meant getting into a few dozen classes and abstracting them, adding the different mutations for multiple viewports only compounds the issue. 

And CSS media queries are not only about viewport sizes anymore. Nowadays we are also dealing with multiple preferences, from limiting animations with prefers-reduced-motion, to choosing a theme with prefers-color-scheme, high contrast mode with prefers-contrast, reduced transparency, prefers-reduced-data, maybe even foldable screens, etc, and the many combinations of them.

This is, in my opinion, Tailwind’s Achilles heel.  The utility-first approach is simply not suited for conditional CSS.

And things are only getting more complicated in the future. CSS is getting so complex that dealing with all those preferences and intersections is probably going to be the greater challenge, one that will forever split the roles in front-end development between the JS specialist and the CSS specialist, giving the latter their long-overdue recognition. I can’t help but imagine someone at the CSSWG smirking in triumph right now, knowing this may have been their plan all along

Conclusion

As we’ve seen, most discussions about Tailwind are rarely scoped to the framework itself, but part of much bigger discussions and its future may lie in the resolution of those issues instead. 

Tailwind provides the possibility for lightning-fast prototyping, great customization options, and a great developer experience, especially if your team is comprised mostly of people coming from imperative programming backgrounds. While it may be the perfect choice for such cases, the increasing complexity of CSS itself could be Taiwind’s demise. If the industry embraces the upcoming CSS features and the role of a dedicated CSS specialist, you may find you’ve invested big time on a huge pile of technical debt. Proceed with caution. 

Are you looking to hire a Front-end developer? You’ve come to the right place, every Scalable Path developer has been carefully handpicked by our technical recruitment team. Contact us and let us know what your needs are.