Tailwind, like everything in life, has its pros and cons. It's important for an ecosystem to have a good balance of criticism and praise in order to maintain a process of continuous improvement and providing its users with diverse opinions that enhance their hability to choose whether to use it or not.
In this article, I will treat Tailwind as a tool, not as a deity, a problem, or a definitive solution. What I hope for is that this article helps in the decision-making process of applying Tailwind to a project from the perspective of application architecture, taking into consideration its pros and cons, rather than purely individual and personal feelings like "DX" (Developer Experience) or "ease of use."
This article expresses my humble opinion based on my personal technical judgment only. How I feel about Tailwind or its community was secondary to me when writing this article, and I hope the same from the people who will read it.
What is Tailwind CSS
Tailwind is a utility-first CSS framework. It's a framework because it's not a standalone utility but rather a small, intricate ecosystem built around a design system. It's utility-first because it provides a set of single-responsibility classes that bring styles with a strong sense of design, minimizing the design decisions that a developer might otherwise have to make, which can sometimes impede the development process.
Positive Aspects
More than just a CSS framework, Tailwind is a PostCSS plugin. This is because its biggest benefits are the generation of Just-in-Time (JIT) classes and the ease of purging, prefixing, and other optimization and compatibility tools that come right out of the box.
JIT (Just-in-Time)
Just-in-time was introduced in Tailwind v2.1 and is the ability to generate styles on demand. By specifying the style entry points in your configuration file, Tailwind only generates the styles that are declared in your template files (HTML, JSX, etc.), ensuring that unused classes don't exist in production.
Purging
Purge is the removal of unused CSS. The purge algorithm has to take into account a series of factors, which can make the removal process challenging for dense CSS code or CSS with conditional logic injected via script. Tailwind restricts various CSS idiosyncrasies and keeps the user within its token and class ecosystem, providing full predictability for PurgeCSS](https://purgecss.com/) to safely remove unused code.
Optimization and Compatibility
Tailwind uses minification and compression to deliver optimized CSS bundles. These are strategies that can and should be applied to any CSS approach that doesn't have them by default. Tailwind utilizes Gzip and Brotli for compression and CSSnano for minification.
DX (Developer Experience)
Developer experience is the quality of minimizing friction in the development process, making the programming experience smooth and almost "effortless." Thanks to its ecosystem of plugins, presets, and extensions, it's possible to rapidly build interfaces with code that autocompletes with the available classes, almost like typed code. Tailwind also offers extensive and visually rich documentation, along with many community-created solutions and abstractions.
When to Apply Tailwind CSS to My Project?
Tailwind is extremely useful for projects that have either no design system or one that is simple or minimally opinionated. It's perfect for projects compatible with uncomplicated CSS and low maintenance, and it's ideal for front-end teams with little knowledge of CSS or limited resources allocated for CSS architecture.
If you're unsure about the terms "design system" or "design tokens," storybook has a nice article about it.
When I talk about applicability, I'm not saying it's impossible to use it in a large-scale application or one with design systems, but when you do, the team will deal with many more points that Tailwind doesn't cover, leading to the creation of abstractions, patches, and extra complexity.
🧐 If you have good examples of an application meeting these parameters that worked well with minimal complexity, please share them; I'd love to learn about them!
Negative Points
I'm greatly bothered by those who criticize anything without using it or based on frivolous criteria. To write this article, I tested Tailwind in two ways - I created an application to test complex UI algorithms, and I read the source code of two popular frameworks that use Tailwind, shadcnUI and DaisyUI.
I made this choice because one of them directly uses Tailwind classes, while the other abstracts them into unique classes using the @apply
directive, taking advantage of its design tokens.
Existing Criticisms
Adam Wathan has addressed one of the recurring criticisms of Tailwind, which is the separation of concerns, maintaining a code separation of responsibilities for each part of the application. While this separation can bring benefits like decoupling, maintainability, and modularization, it remains an opinion of some writers within specific contexts and should be taken into consideration, not treated as an absolute law or axiom.
There is little measurable evidence of benefits from separating HTML and CSS beyond the file level. Even at the file level, it's a good performance practice to place critical CSS inline within a <style>
tag.
HTML that appears illegible to those who don't like or work with Tailwind is actually one of the fundamental premises for teams working with this framework. If you split your templates into pages and components, you need to revisit the templates less often and deal with smaller code segments. Eventually, you become accustomed to it or install a linter.
In my view, the root problem precedes the issue of ugly templates. The problem lies in the structural failure of style reuse.
Style Reusability
This passage primarily considers the "Reusing Styles" chapter from the official documentation.
Tailwind is incapable of handling code repetition on its own. Its methods for dealing with this issue are highly tied to the IDE or to libraries and frameworks for templating or components, such as React, Vue, or even vintage ones like Nunchucks.
Editing via Multiple Cursors in the IDE
A viable solution, but it assumes the use of an IDE and a specific feature that has low accessibility and is impractical to use across documents (through global find and replace) because it can select classes beyond the intended scope. However, this can be addressed by locating these scopes within components.
Extracting and Componentizing
Components can be easily reused, but they still involve a form of coupling that assumes at least a rudimentary form of templating and post-processing. Of course, it's possible to use Tailwind in vanilla projects, but not without losing the benefits of JIT (Just-in-Time), compression, and directives whose support comes entirely from PostCSS. Without PostCSS and processing, Tailwind's most significant qualities would not survive. Even in a context where components are used, CSS provides various reuse tools, such as non-utility classes and selectors. Tailwind can only be leveraged through the @apply
directive.
@apply Directive
This directive exists because it's the easiest way to create classes that still utilize Tailwind's design system. It's possible, for example, to use Tailwind's design tokens because it uses CSS variable composition but doesn't expose them as an API, doesn't document them, doesn't JIT them as standalone, and lacks autocomplete, which can compromise the integrity of Tailwind's design system. Not to mention that Adam himself acknowledges the excess complexity and prominence of bugs.
Again, you may be okay with this, especially if you work within an ecosystem that aligns with all these requirements. Understanding this limitation also means understanding when to make the most of it when it becomes necessary.
Tied CSS
Much of Tailwind's Developer Experience (DX) is due to the abstraction of fundamental CSS concepts such as compound selectors, CSS units, CSS functions, inheritance, and specificity. While these concepts may indeed be complex, they also enable the creation of concise, robust, retro-compatible CSS with room for progressive enhancement.
I've created three cases with varying complexities to demonstrate this.
Holy Grail Layout in Josh Comeau's Approach
Inspired by Josh Comeau's article "Full-Bleed Layout Using CSS Grid - An elegant solution to a tricky modern layout."
This layout, when applied responsively without media queries, can be achieved with just 5 lines of CSS, 8 if you don't use native nesting:
.grail {
display: grid;
grid-template-columns: 1fr min(var(--width, 800px), 100%) 1fr;
& > * { grid-column: 2; }
}
Of course, there are other ways to achieve this layout, but they involve more CSS - media queries to control the side widths based on device width or more HTML by creating containers and wrappers using the same logic as the Bootstrap Grid..
In other words, to produce the same layout, you need to introduce unnecessary complexity, with the sole benefit being "development experience."
You write "less CSS," and you don't need to know complicated grid algorithms, but you end up with m*ore templates to process, test, and debug*. While utility classes are easy to debug because they contain only one property, pages or templates can easily turn into a wrapper hell when the template assumes the responsibility that styles should provide.
Arbitrary Values
In the latest version, it's possible to create a class with these parameters on-demand using the arbitrary values API.
<section className="grid w-full grid-cols-[1fr,min(var(--width),100%),1fr] place-items-center gap-y-4 [&>*]:col-start-2">
<!-- ... -->
</section>
But the limitations are clear:
It's only possible if you use a very specific syntax. The syntax is not simple and doesn't support spacing, making it easy to introduce bugs and very difficult to identify them.
You can use selectors like [&>], **but they only accept one parameter at a time, and the generated code is practically unreadable*.
/* Output da classe [&>*] */
.\[\&\>\*\]\:col-start-2>* {
grid-column-start: 2;
}
"But the output doesn't matter"
You may not care about the output, but the browser doesn't care which tool you're using. If there are errors or inconsistencies, you'll face a devTools with dozens of classes, each with its encapsulated context and others with unreadable declarations.
Stay tuned, there will be a part two
There's a lot more to be said and examples to explore, so I'll create a second part analyzing another application of advanced CSS, whether Tailwind actually produces less CSS, the issue of inheritance and specificity, and Tailwind's interaction with other frameworks and foreign code.
I'd love to hear your opinions up to this point. I challenge you to set aside personal biases and preferences because, personally, I've learned a lot technically by examining this tool objectively, and I hope the same happens for you. Don't be like Adam:
Top comments (4)
Great article!
I never delved too deeply into Tailwind, but I confess that I have used it for small projects, where I didn't want to spend a lot of time writing CSS (and dealing with all its complexities). I really appreciate the innovation that this framework brought, making it possible to write complex styles just through classes in HTML, but I have the impression that it is much more difficult to maintain a large-scale project with it (I am an advocate of separation of concerns).
An interesting point you mentioned is that Tailwind can also be interesting for teams with little knowledge of CSS - hence its great popularity in the development of templates for download/purchase.
I believe we still haven't founf a great tool for writing CSS, but definitely for me Tailwind is by far not the best solution for replacing preprocessors and CSS-in-JS. Perhaps I would limit its use in very specific scenarios where low CSS knowledge and development speed are the main keys (a POC, prototype or simple application for example).
Hello! Thank you for the comment!
So, I think it doesn't even aim to replace these tools; it's the community that sees it that way. Another scenario I should have included is that maintenance is an investment, it costs money and brings a low perception of return on investment. Verbosely written and limited code and clean and modern code can produce very similar quality UIs. Tailwind is designed so that if your case is the former, you don't have to look back to maintain it.
The greatest quality of Tailwind is not worrying, and because we are increasingly paid less to worry about CSS, the trend is that the trade-offs of solutions like this will seem increasingly insignificant.
Thanks for your article!
I like that you look at both sides of the coin. In my opinion many things in Software Development are a trade-off. I am looking forward for part two.
I did and wrote the experiences of my trade-off with tailwind here, if you are interested - My journey and experienced trade-off with Tailwind CSS - I took the liberty of copying one of your examples, I hope that's cool with you.
In the world of programming, there is almost no silver bullet.
I think Tailwind is no exception.