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.
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.
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.
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.
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?
Pollen is a highly configurable library of CSS variables for your next design system. It lets you write faster, more consistent, and more maintainable styles.
Made and maintained with ❤️ by the fine people at Bokeh.
Features
Robust library of well-considered, style-agnostic CSS variables
Fully configurable and extensible with CLI build tool
Zero setup required to get started
Responsive with configurable @media and @supports queries
Lightweight, human-readable output if you ever want to move away from Pollen
What it looks like
Pollen's design tokens can be used to build any project. They're easy to customise and extend and they don't require preprocessors, class naming conventions, or non-standard syntax. Generate an entirely custom design system with a simple config file.
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.