Tailwind CSS has taken the frontend development world by storm over the last few years. A utility-first library of CSS classes, it promises a new way of styling that's more consistent, maintainable, and faster than writing CSS directly. And for the most part, it delivers on that promise.
By using Tailwind you're almost guaranteed a single source of truth for all the values you use throughout a project. From typesets to spacing to colours, everything is defined in a single place. Which means that your code stays consistent and you aren't making things up as you go.
This was Tailwind's biggest idea, and the greatest benefit of utility-first CSS as a concept: compose don't create.
Tailwind achieves this with an extensive library of CSS classes to style everything. The idea being that you no longer write any CSS of your own, you compose predefined classes like lego pieces for every single property.
Developers new to this way of working often have a knee-jerk reaction just from looking at example code.
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Button
</button>
There's no denying that Tailwind is hideous. Its creator acknowledges as much right on the project home page. But that's just pedantry, and if the Tailwind way of doing things really was the panacea to all our problems then it would be a very small price to pay.
The Problem
The problem with this approach isn't that its ugly, or bloated (Tailwind purges unused classes), or that "you might as well write inline styles" (you shouldn't). It's that in order to apply a consistent set of values with classes, you also have to create classes for every conceivable set of rule:value pairs in CSS, even where it adds no value at all. So you end up using classes like .block
rather than writing display: block
and .text-center
rather than text-align: center
.
Of course you can mix Tailwind's more useful classes with regular CSS. But then you're breaking the Tailwind style-by-classes abstraction, and you have to maintain two seperate styling touchpoints for every element.
"So what?" you might ask, what's wrong with just using those classes rather than CSS? It certainly saves some keystrokes. Here is where Tailwind introduces new problems it shouldn't have to solve in the first place.
Reinventing CSS
Tailwind has to reinvent everything regular CSS can already do. Media queries, pseudo elements, selectors, and states. All of it now has to fit into the classes-only paradigm.
Tailwind achieves this with what it calls modifiers. By prepending Tailwind classes with md:
they will only apply above the md
breakpoint. By appending hover:
a class will be applied in a :hover
state. And so on.
Each of these tools is a poor facsimile of the functionality gaps it has to fill. Want an :nth-child
or ~
sibling selector? Back to CSS. Want to target the devices between two breakpoints? Back to CSS. Want to target children of an element? Back to CSS. You get the picture.
Of course you can go back to CSS to do any of these things. Lovingly coined "bailwind", almost every project will need at least a little custom CSS when Taiwind's classes and modifiers just don't cut it. But then you're back at breaking the Tailwind abstraction, and giving yourself maintenance headaches.
And if this is already a given, then why use pointless classes like block
when it adds no consistency or maintainability value over writing display: block
in CSS, other than a few saved keystrokes? Because while gap-filling classes like this don't add value, they do add a new Domain Specific Language (DSL) to learn on top of the CSS we all already know.
Class soup
The thing every critic of Tailwind yells at first, its enormous class strings. Yes, they're ugly, but who cares. The problem isn't a surface-level developer perfectionism one. It again comes back to modifiers.
Every rule that applies to a modified state needs its own class with its own modifier. Unlike in CSS where these states and pseudo elements are naturally organised into logical blocks, Tailwind's modified classes can very quickly become a huge, difficult to maintain mess that has to be carefully teased apart line by line.
Take a contrived example of the button we had in the intro of this article, with an icon of some sort added to a ::before
pseudo element, and less-than-ideal attention given to class ordering.
<button class="relative before:absolute bg-blue-500 hover:bg-blue-700 text-white before:left-2 font-bold before:text-sm py-2 px-6 rounded before:top-1/2 before:content-['\f00c'] before:-translate-y-1/2">
Button with icon
</button>
Of course in this particular example the icon would be better placed as a real element inside the button, but the point stands. Without (and even with) careful ordering of classes, these jumbles very quickly become a maintenance nightmare.
JIT to the rescue?
Tailwind's new Just In Time mode compiles just the classes you use on the fly, rather than pruning back a goliathan stylesheet after the fact. It allows you to use modifiers everywhere out of the box, and most importantly write arbitrary values right in Tailwind classes, like margin-[100px]
.
This is another language feature that was added to Tailwind's style-by-classes DSL in order to fix problems it introduced itself. And while arbitrary values mean you don't have to break out of Tailwind's paradigm as often, they also diminish the core value that Tailwind provides β a single source of truth for a whole project. Taken to its logical extreme Tailwind JIT is really just reinventing CSS, bit by bit.
The Solution
As I said at the very beginning, Tailwinds' central idea is a very good one β a low-level, utility-driven design system to get rid of magic numbers and bring consistency to your CSS. The problem was the implementation.
Thankfully CSS now has the same solution as every other language to consistent values: variables. CSS variables, or more properly CSS custom properties, are fairly new to the language but already adopted by every major browser, and used extensively in Tailwind's own internals.
For example, Tailwind's .p-4
padding utility could be rewritten like this
:root {
--p-4: 16px;
}
.button {
padding: var(--p-4);
}
And since we no longer have to write separate classes for every rule:value pair, we can greatly simplify our utility-first design system. We could have one set of size variables that can be applied to any part of padding, margin, width, height, position, etc. Without needing separate utilities for every combination of every property.
:root {
--size-2: 8px;
--size-4: 16px;
}
.button {
padding: var(--size-2) var(--size-4);
margin: var(--size-4) 0;
}
And since variables are part of the platform, they have a native runtime. We can interact with CSS variables using Javascript, and update them dynamically. This makes things like reskinning a whole interface for dark mode possible with just a couple lines of code, without introducing any new utilities or tools.
function enableDarkMode() {
document.documentElement.style.setProperty(`--color-background`, `black`);
document.documentElement.style.setProperty(`--color-text`, `white`);
}
So why don't we, instead of reinventing the styling paradigm altogether, just abstract all the values in an interface into a single source of truth by putting them in CSS variables that can be used anywhere, in regular old CSS, without all these new problems?
By Example
peppercornstudio / pollen
Utility-first CSS for the future
Pollen
Utility-first CSS for the future
Introduction
Pollen is a standards-driven alternative to Tailwind. A micro-library of CSS variables, it encourages consistency, maintainability, and rapid development. Use it from prototype to production, as a utility-first foundation for your own design system.
What it looks like
Pollen's low-level variables can be used to build any design. They work anywhere and don't require a buildstep or class naming conventions. They're easy to extend and globally responsive, without introducing preprocessors or new syntax.
.button {
font-size: var(--scale-00);
font-weight: var(--font-medium);
padding: var(--size-2) var(--size-3);
background: var(--color-blue);
border-radius: var(--radius-sm);
box-shadow: var(--elevation-2);
color: white;
}
Documentation
Read the full documentation at pollen.style
Pollen is a standards-driven CSS library that came from this line of thinking. It takes Tailwindβs core ideas and reimplements them as a 0.85kb collection of plain CSS variables. It can deliver all the key benefits of Tailwind without reinventing how we write CSS, thanks to the tools we already have in the web platform. Since itβs just plain CSS it can be used anywhere in any context and in any way.
But you might not need it
Full disclosure: I wrote Pollen. But I'm not trying to sell you on adopting it. I'm trying to sell you on the ideas behind it for your own work.
Pollen is just a collection of hopefully useful CSS variables, translated from Tailwind. But If you already have a solid design system with sizes, typesets, colours, and the other shared values of an interface, then you probably don't need Pollen, and you certainly don't need Tailwind. Write them in one place as CSS variables, and use them everywhere. That's the way out of this insanity.
Bring consistency to CSS by getting rid of magic numbers with variables. The other problems of CSS (deep composition, leaky inheritance, performance optimisation) aren't solved by Tailwind, nor by CSS variables. But they are made harder by the new DSL Tailwind introduces. At least by sticking to regular CSS you have all the other patterns and tools we as a community have been working on for the last decade at your disposal, without any gotchas.
Latest comments (110)
Hey guys, this has gotten a lot spicier than I anticipated, so I'm going to go ahead and lock comments on it.
A few things to reiterate before I do:
Tailwind is a solid library written by smart people, with great ideas behind it. I think the way these ideas are implemented introduces serious problems at scale, which often go unmentioned. If those problems aren't deal breakers for you, then by all means use Tailwind!
The other library I talked about, Pollen, is just a collection of CSS variables. It shows how Tailwind's key benefits can be accomplished using the tools we already have in the platform. I hoped that some people might find it useful, I know I have. But as I mentioned in the article, you absolutely don't need to use it, and I'm not trying to sell you on switching to it.
Apologies to anyone this article offended. That wasn't the intention
β madi
I agreed with some of the criticisms of Tailwind (though I wrote my own library/framework, Passionfruit, for a similar purpose and in a similar manner, long before I knew anything about Tailwind (probably before it existed, but I haven't checked)).
However, I wasn't expecting this to be a run-in to advertise another library. I feel like this should've been stated clearly from the beginning. Just feels dishonest, imo.
Stuffing CSS variables into a tailwind config file seems to be a popular route, so cheers for implementing the principles of Tailwind with a CSS-first approach; not requiring config files and build tools to get there.
Before I clicked into Pollenβs docs I was looking for your take on Tailwindβs
@apply
but realize now you donβt need to mention that since your point is about configuring utility classes through JavaScript vs configuring them through CSS.Great to have the alternative way to do things π
I don't really see how using CSS variables is better.
From your example :
You can do that in Tailwind with
@apply
like :There is clearly no advantage to the variables.
Also, Tailwind is really useful to work with designers (and with Figma for example). They use a Tailwind template and you can clearly see the padding they want, you have the color and hue gradient applied, the text size, etc. You just have to type the Tailwind class corresponding.
BEM + Tailwind is for me really versatile. And if I need an extra responsive margin on an element just one time, I can do it in no time (exemple
class="button mt-2 md:ml-2 md:mt-0"
)[Edit] :
And totally forgot about useful classes like
divide-x
divide-y
space-x
space-y
dark:
ring-
I haven't used Tailwind, but enjoyed this article nonetheless. I'll keep its arguments in mind, and if I ever consider using Tailwind I'll evaluate said arguments based on their merits and drawbacks in relation to my specific use-caseβas one part of several in a broader evaluation.
I also appreciate that the author based on an analysis of a perceived problem came up with an example solution (which is pretty much our collective job description). I think that's a commendable mindset, no matter what you think of the solution specifically. It's a shame that what could have been a quite fruitful mutual discussion has degraded to schoolyard antics, here in the comments.
@madeleineostoja I beg you, let this not be your last article, hate comments just mean you had something really worth sharing. You wrote this perfectly well, this is basically the model for a good research work. Bringing light to both advantages and disadvantages, your findings, then you ended it with your solution. Come on, that's how it's to be done. The people who really deserve to criticize the shortcomings of something, are those that are able to produce a better way of going about it, and that's exaclty what you did.
Thanks for the great write up, and please keep it up.
For all who thinks this is a way to sell in Pollen, read the article:
Full disclosure: I wrote Pollen. But I'm not trying to sell you on adopting it. I'm trying to sell you on the ideas behind it for your own work.
Good points and valid arguments in my point of view.
I'm not saying Tailwind CSS is a magical solution to everything but all of your examples can be done "the Tailwind way" as well.
Tailwind has selectors for
:first-child
,:last-child
,:odd-child
and:even-child
. Those cover most of the use cases but in case you want a class only on the 7th-child, the easiest way is to just put the class on that element instead of using the pseudo-selector on the parent element. Or if you are using a front-end framework to dynamically add the elements, you can add the logic of adding the class to the 7th child in javascript.For siblings, usually you'd just add the classes to the element itself but you could also use the peer-* variants if you need to depend on a sibling element's state.
I'm not completely sure what you mean with this but if you only want a class to be applied to let's say the
md:
breakpoint then you can just set the style back to the original onlg:
. For examplebg-black md:bg-red-500 lg:bg-black
is red only if the window is 768 - 1024px wide.The whole idea in Tailwind is to apply the styles for each element itself so ideally you'd add the class to the child element. If that's not possible then you can create a selector for it yourself or use tailwindcss-children -plugin for example.
Cool...
Tailwind is the answer to huge projects, read the customizing section in it's documentation it will solve many of your problems.
It is a good article. It is sad some fan boy try to resolve their soul and ego problems here in the comment section. You should write more article and don't worry about these kind of people. Keep it up! Your photos are awesome btw :-)
Thank you so much!
Btw your menu drawer on pollen.style is not working on mobile. Maybe fix it by using tailwind.
Itβs just a site on gitbook, but hope that was fun to point out
The integrity of this post is hilarious - you're bashing on a CSS framework you obviously don't know enough about, just to promote your own CSS framework π€¦ββοΈ
She isn't bashing on anything, she literally points out the good qualities of Tailwind right in the beginning.
I agree 100%, tailwind is reinventing css in a bad way. It's inline css under some shorter names and in class instead of style -_-
My friend, I sense you miss one big point in here.
The reason why people used Bootstrap and now Tailwind CSS is not consistency, but rather getting away from the hell of CSS.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.