Why Tailwind CSS
Why I changed my mind on Tailwind CSS, and why I now consider it the Goldilocks Styling Solution
I'm not a Tailwind shill. I'm a Guo Lai Ren - someone who has changed their mind on it recently and am a happy user despite acknowledged tradeoffs. "Crossover people" can often be more persuasive to skeptics than born-and-bred believers. So I hope to contribute my perspective to the discussion, if you are open to it.
A while ago Adam Wathan asked: "Did you think Tailwind was a horrible idea until you actually built something with it?"
I once complained to @samselikoff that Tailwind caused ugly unreadable classname soup and said zero-runtime CSS-in-JS could do more with a lower learning curve.
I was wrong on 2 counts: Tailwind is easier to learn than I thought, and CSSinJS's flexibility can be a negative.
After shipping a few projects (including my personal site and book site) with Tailwind now, I feel I should probably jot down my thoughts on what I like about it. Since Tailwind is the predominant Utility CSS framework and the only one I've tried, I'll make no effort to distinguish the points below from the general benefits of Utility CSS (but here's a list of others).
- "System" Values reduce Magic Numbers: Decrease hardcoded values, Increase consistency.
- Responsive Design in the Browser: Prototype in browser, copy and paste to codebase, using consistent system values.
- Inlining Styles Optimizes for Change: Make code easy to delete and move, by eliminating all reliance on the cascade.
- Inlining Styles reduces Naming: Ship faster by solving one of the known hard problems in Computer Science!
- Zero JS & Sublinear Scaling of CSS: Scale at O(log N), not O(N).
- Utility-First, not Utility-Only: Respect the Principle of Least Power, use CSS-in-JS only when warranted.
Before you listen to me, you may wish to check out the most influential pieces on the "utility classes" revolution in CSS right now:
- Adam Wathan's blogpost on CSS Utility Classes and "Separation of Concerns"
- Simon Vrachilotis' talk on A Real Life Journey into the Opinionated World of Utility-First CSS (see also: Sarah Dayan's talk)
I am heavily influenced by these people and others, so if I repeat some points poorly below, the fault is mine. At least I gave you the canonical sources first.
CSS is extremely flexible, which makes it powerful, but also gives you a lot of footguns to shoot yourself with. Constraints are needed, or else we sprinkle "magic numbers" all over our codebase.
Magic Numbers in CSS are a bad thing, says Chris Coyier. He defines it to mean "values which “work” under some circumstances but are frail and prone to break when those circumstances change", but honestly any hardcoded number, like
px in margins and media queries, or color variants, is difficult to manage well at scale. The temptation to break rules just to ship a fix is there, and the difficulty of refactoring when design requirements change is too high. If you are working by yourself, there is nothing enforcing that you stick to a consistent set of well designed number scales, which can lead to bad-looking design.
The solution, of course, is to draw only from a preset range of number values, which I call a "system" (note that I don't call this a "Design System", a debate I have no interest in getting into). Tailwind comes with a good set of font and color systems by default. Again, you could try to roll your own system with CSS variables, but you don't have Steve Schoger on your team.
This point is most relevant to developers-who-do-design: the best development workflow is to preview your site on
localhost, make adjustments in the browser until you are happy with it, and then copy-and-paste your changes straight into your codebase. Let's call this the Design in Browser workflow.
If you want this workflow, you rule out using React's inline styling (which make you use object syntax). But let's say you do use some form of "Write Real CSS"™ solution like Styled-Components or Vue or Svelte scoped styles, where design-in-browser is possible. What else does Tailwind offer you?
- You can pull directly from preset "system" values (elaborated above) while prototyping in browser
- You can do responsive and pseudo-class design while in browser too - e.g. to apply styles at different breakpoints, or on hover or focus, you can just prefix inline, e.g. for a link,
text-green-400 hover:text-green-300 md:text-blue-400.
I did a demo of this in a recent video:
Designing in the Browser is not quite Bret-Victor-style Inventing on Principle, but you are getting at least closer to being able to "play" in context by reducing the cognitive distance yet again. With Tailwind, you can even add transitions and animations inline while you play. This is extremely underrated - we developers might offer more movement in our apps if only it were easier to prototype and add them.
A lot of production CSS is append only. This is because the cognitive distance between the CSS and the markup it affects is often far - sometimes in a different folder, different file, or same file but dozens of lines away. On top of that, you have to remember the CSS cascade and run every element against every matching rule, in your head.
Pretty soon, you have a codebase you are scared to even open! Developer velocity slows down, and eventually you back yourself into a corner where nothing less than a full rewrite will do.
By design, CSS is easy to extend. Just add specificity! But it is not easy to delete. This adds complexity, in the best Rich Hickey sense of the word (because position matters in CSS, you now have to remember all positions). It's easy to build up the house of cards, but take one thing out and the whole edifice may fall apart, and you WON'T KNOW until you check for visual regressions or emulate the browser in your head.
You can use tooling (CSS modules, static CSS in JS, Vue or Svelte scoped styles) or naming conventions (BEM, etc) to control specificity, but that reduces the cognitive distance rather than eliminates it. The only option with zero "spooky action at a distance" is inline styling. Inline styling optimizes for change.
"Galaxy Brain" time: Tailwind offers the developer velocity benefits of inline styles without its downsides.
Alternatives exist: Other solutions like Emotion's
styled-jsxoffer similar benefits of inline styles, but they run into the standard CSS in JS downsides
Naming Things is a known hard problem. We waste a lot of time bickering over naming classes. With Styled-Components, you often write a bunch of intermediate styled components you have to name. With BEM, we replace one naming problem with three and a half naming problems (I've had PRs held up on whether I should've used
__ - what a total waste of time). How many millions in developer-hours do we waste every year bickering over names?
With utility CSS we significantly reduce the total number of names in our codebase, and perhaps more importantly, the number of names we have to independently invent and remember. This feels minor until you've worked on a codebase where it isn't. What price are you willing to pay to eliminate one of the known known hard problems? I'm not kidding - this is a conversation worth having. Names don't matter to machines but they matter to humans.
The tradeoff is you have to learn the names from the utility CSS framework.
space-x-reverse aren't parseable without docs. The difference is that traditional CSS naming is bespoke per project, whereas you learn Tailwind once and can use them in every project. Yes, you could try to roll your own utilities, but Tailwind's naming is probably more thoughtfully designed than whatever you come up with.
Alternatives exist: Emotion's
styled-jsxalso let you skip naming.
A lot of ink has been spilled about the performance tradeoffs of using CSS in JS, and their mitigating factors. You can check my What's New in React talk for more, but rest assured it is hotly debated with passionate, intelligent people on both sides. But we all agree that the less JS you ship, the better, and we also agree that byte-for-byte, shipping 1kb of JS has a far bigger performance impact than 1kb of CSS. Those are well understood.
The point I'm keen on exploring here is that many CSS and CSS in JS solutions scale linearly with the number of components in your app. Because CSS scopes each declaration to your identifier, you have to repeat it everywhere you want to apply it. This is how we ended up with >50 declarations of
font-weight: bold at a previous workplace. Individually, these don't matter, but in bulk, they add up.
"On our old site, we were loading more than 400 KB of compressed CSS (2 MB uncompressed)... We didn’t start out with that much CSS; it just grew over time and rarely decreased. This happened in part because every new feature meant adding new CSS." - Facebook Engineering
You can defer this problem with code-splitting, but eventually people ship and implement hacky workarounds to the point where the CSS gets out of control again (particularly if it is append-only!).
The solution here is (arguably!) to ship "atomic" CSS, so that your CSS scales by
O(log N) instead of
N is your number of components). Facebook's unreleased stylex library lets you write CSS in JS and generates atomic CSS for you, but you could also just choose to hand-write atomic CSS, which is what utility frameworks like Tailwind guide you do.
To be fair, I put this point at the bottom, because it is unlikely that most apps get to the scale where this really starts to matter, especially when taking gzip into account. However, like with all optimizations, these things are premature until they are not.
However, the real life usecases in which you actually need to do this are limited, and the costs (in JS weight, for example) dominate when you just use it for static styling. Going utility-first respects the Principle of Least Power here.
Non-CSS-in-JS solutions are often also easier to debug. "Easy" here is of course subjective. But when things go wrong with CSS in JS solutions, I have often found myself getting to a point where I had to look up docs, then GitHub issues, then diving into
node_modules, which is a lot of yak shaving away from what I really want to be doing. When things go wrong with Tailwind, I know that I'm either generating the classname I expected, or I am not. There are much fewer points of variance. But it stands to reason that you should use the easier-to-debug solution most of the time if you can.
Is Tailwind perfect? No, of course not. But the good outweighs the bad:
- Setting up Tailwind means fiddling with build tooling. This is getting easier, and is comparable with tooling required for similar performance with other solutions, but is unacceptable to some.
- The Tailwind API surface area is big and constantly growing. It's understandable (it has to map all of CSS!) but also can be tiring to learn and keep up with. The end result is you pay some upfront learning cost for hopeful long term productivity gain. Nice, but it isn't a pure win.
- The classnames do get rather verbose. It'd be nice to have shorthands like
md:hover:(text-green-300 underline border-5), but then that'd just add API surface area. (Edit from the future: this is now being implemented in
oceanwind) Perhaps use of
@applyis warranted, or just some smart typography.
- CSS abstraction leak - Tailwind let's you use classes like inline styles, but they are NOT inline styles in one critical respect - what happens when they clash. Eg with 'm-4 m-2', the "m-4" takes precedence. The order of classes generated by Tailwind matters, even though it is invisible to you. This is an abstraction leak. If you are trying to build reusable components for an in-house design system, this runs counter to that. You may wish to explore Chakra UI instead.
- Project governance owned by Tailwind Labs - they are of course the project originators and great stewards for now, but it isn't an egalitarian process with established conduct like the CSSWG. As with all BDFL relationships, it's fine until the day you disagree with them.
- (minor) The VS Code extension still isn't as robust as it could be - requiring a space to initiate and often just not working. But Brad Cornes is on it!
Above all I think choosing Tailwind is a matter of personal preference rather than being the objective right answer. There is a wide spectrum of styling solutions from super opinionated to not. This is how I put it recently:
- Premade Component Libraries are too restrictive, Vanilla CSS is too permissive
- CSS in JS is too heavy, inline CSS is too underpowered
- We want design system constraints, but the way we tap into design systems have been very heavy handed (bound to framework)
- Ben Holmes: "a solid in-between for designers that want freedom and devs that want structure".
Tailwind is for those who desire a styling solution that is "not too hot, but not too cold".
The Goldilocks solution.
- Johan Ronsse: Why you’ll probably regret using Tailwind
- If you have a notable "Why Not to Use Tailwind" or "Why I use something else despite knowing about Tailwind" post, let me know and I'll include it here
- On a recent Svelte Radio (52mins in), Kev mentioned that people are having tooling integration difficulties. This will be better documented over time.