swyxio / swyxdotio

This is the repo for swyx's blog - Blog content is created in github issues, then posted on swyx.io as blog pages! Comment/watch to follow along my blog within GitHub
https://swyx.io
MIT License
334 stars 45 forks source link

Why Tailwind CSS #206

Closed swyxio closed 2 years ago

swyxio commented 2 years ago

source: devto devToUrl: "https://dev.to/swyx/why-tailwind-css-2o8f" devToReactions: 350 devToReadingTime: 12 devToPublishedAt: "2020-10-04T02:48:22.841Z" devToViewsCount: 15444 title: Why Tailwind CSS published: true description: Why I changed my mind on Tailwind CSS, and why I now consider it the Goldilocks Styling Solution category: essay tags: Tech, CSS, Tailwind, Reflections slug: why-tailwind canonical_url: https://www.swyx.io/why-tailwind cover_image: https://dev-to-uploads.s3.amazonaws.com/i/ubgj091em2q1ri9mmwth.png

Translations: 中文, Português

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 replied:

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).

TL;DR

I also cover popular objections to Tailwind at the end with links to their arguments so you can decide for yourself.

The Utility-First Canon

Before you listen to me, you may wish to check out the most influential pieces on the "utility classes" revolution in CSS right now:

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.

"System" Values reduce Magic Numbers

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 the syntax for that is verbose and you still have to come up with names for classes and for variables, and you end up with a custom system that doesn't transfer across projects and probably isn't well documented. Might as well adopt a good default system by importing Steve Schoger on your team.

Alternatives exist: Styled-System and Theme UI from the jxnblk-verse are the basis for this in CSS-in-JS land.

Responsive Design in the Browser

2021 Edit: Tailwind's new JIT mode now negates this point :( Our discussion here

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?

  1. You can pull directly from preset "system" values (elaborated above) while prototyping in browser
  2. 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:

{% youtube 1LYoYBeig6g %}

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.

Inlining Styles Optimizes for Change

IMPORTANT note: Utility classes are NOT the same as inline styles - I am using "Inlining Styles" as a shorthand, but the reality is more nuanced and VERY in favor of utility classes.

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 css prop and styled-jsx offer similar benefits of inline styles, but they run into the standard CSS in JS downsides

Alt Text

Inlining Styles reduces Naming

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 -- or __ - 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. -mb-5 and 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 css prop and styled-jsx also let you skip naming.

Zero JS & Sublinear Scaling of CSS

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 O(N) (where 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.

Utility-First, not Utility-Only

On top of all the above benefits, you can STILL use those other solutions for benefits you need. For example, nothing is truly as powerful as CSS in JS, where you can dynamically change out media query values and entire rulesets based on arbitrary JavaScript.

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.

The Bad Parts

Is Tailwind perfect? No, of course not. But the good outweighs the bad:

Conclusion: The Goldilocks Styling Solution

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:

Tailwind is for those who desire a styling solution that is "not too hot, but not too cold".

The Goldilocks solution.

Alt Text

Appendix: Opposite Perspectives

Addressing Objections (Jan 2021 edit)

swyxio commented 1 year ago

Remy Sharp has a popular take covering the broader history of CSS leading up to Tailwind: https://frontendmastery.com/posts/the-evolution-of-scalable-css/