loading...
Cover image for Why Tailwind CSS

Why Tailwind CSS

swyx profile image shawn swyx wang πŸ‡ΈπŸ‡¬ Updated on ・11 min read

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

  • "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.

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 you don't have 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

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:

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

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:

  • 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. Perhaps use of @apply is 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!

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:

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

Alt Text

Appendix: Opposite Perspectives

Posted on by:

swyx profile

shawn swyx wang πŸ‡ΈπŸ‡¬

@swyx

Infinite Builder πŸ‘·πŸ½β€β™‚οΈ I help people Learn in Public β€’ Author, the Coding Career Handbook (https://learninpublic.org)

Discussion

pic
Editor guide
 

@swyx nice write up as always and I agree overall
I am personally getting my hands dirty with tailwind currently.

One objection though

Perhaps it was not clear to me or perhaps not clearly expressed but Inlining Styles reduces Naming is not clearly a benefit if you ask me.

If we let all those long arrays of classes that make up a tailwind component inside HTML in production we are trading difficult BEM naming for ease of changing right in HTML but we are also inheritting bigger (in size) HTML files and more difficulty in keeping consistent components.

I believe even in the docs it mentions that we are to extract those classes into other classes with @apply tailwindcss.com/docs/extracting-co... especially if we are to use them in more than one place and in order to reduce the HTML sizes. With this CSS file sizes should not go up much I believe.

It is probably the most controversial point about Tailwind and when people see it it does seem like adding styles in the HTML which we rightfully succeeded (as a community) to overcome a long time ago and this is the point that @nektro is making in a different comment in here.

If tailwind or a tailwind plugin finds a clever way to extract those arrays of classes into meaningfully named custom classes than it will be a step ahead of the competition.

Until than I believe either way of styling big codebases (with tailwind or without) is a test in organizational skills.

  • also a typo perphaps you have "known known" in the text
 

thanks giorgos!

no, "known known" is on purpose.

i dont agree with your point here. the point is that you can achieve consistent components through other means, and you should push styles down to the html element as far as possible. @apply isn't meant to be used extensively, according to Adam. in short, yes, we do want to add styles in HTML, that is what is so hard to accept because it goes against conventional wisdom.

 

Shawn
Yes those are some nice TIPS from Adam in the video you linked to, and yes he does mention not overusing @apply (which I was not aware about) but that is a general statement and a recommendation but I still think it is actually to performance's benefit to use it as much as possible.

I don't like the idea of a component that is full of tailwind classes cluttering HTML and making it heavy in size. I would create custom app classes (using @apply) and add those classes as class values inside my component instead in order to create lighter HTML files.

Imagine a component that gets created 100 times in a loop with tailwind classes taking most of the HTML space. I would naturally want to use @apply to reduce html size and css size would not increase proportionately I believe (might be wrong).

I might be going against the flow or some might say misusing tailwind here but I think we ought to ourselves and the community at large to make our systems as performant as possible when we can.

How does abstracting CSS strings improve performance? If anything, it will reduce performance across all pages the more you perform these @apply abstractions.

You are moving bytes regardless, but you are advocating for moving bytes from an individual HTML file, that is not shared across pages, to a CSS file that will be shared across pages, regardless if the classes are used.

Example:
Page 1 has 30 utility classes
Page 2 has 200 utility classes

In your approach, you would abstract all 230 classes into your CSS file, and ship that file to both pages. Wouldn't you rather optimize each page, so that people who opt into viewing Page 2 have to actually load that HTML?

If you abstract every long class string, most of which will not be reused, you will continually add to your final CSS bundle that will be loaded on every page in your website.

By using the utility classes, you are essentially code-splitting the CSS at the page level, instead of pushing all CSS to every page.

no point discussing performance tradeoffs without actually measuring. if you want to do a benchmark I would be happy to take a look at it and help to publicize it.

@swyx my point can be argued without actually measuring exact performance gain. Simple case mentioned above making it a little more concrete:

instead of multiple times adding the same tailwind classes to a card component extract the classes and use @apply them to custom class and use those in your html, after reusing card a 2nd time you already saved some of space for the html file the more you repeat the component the more you save on html space ... as simple as that.

If I get a chance I will create a demonstration but no need for performance testing.

nothing in perf is simple. how do you account for gzip of your html?

True gzip might be compressing and mitigating for all the repetition

You're not saving any file size though, you're just moving bytes from the HTML into the global CSS. That's what I'm trying to explain. Why would you try to reduce a singular page's filesize and move that into the global CSS for all pages? People should download only what they need, so it makes more sense to bloat the HTML because it's scoped to the page.

this is why I don't bother engaging in perf debates without a benchmark haha.

I never said I would ship this css code to every page regardless and perhaps it is my bad for not mentioning.

This css would be included only if the component is included but only once and not be repeated within each occurance of the component. In case of one instance of the component I don't have any gains but in case the component gets included multiple times (for loop, a listing page) the gains could be substantial.

 
Sloan, the sloth mascot Comment marked as low quality/non-constructive by the community View code of conduct

goes against conventional wisdom

it goes against the fundamental structure of the web. html for layout, css for style, js for interactivity. if you wanna do it a different way, then web might not be for you.

 

You write so clearly Shawn. I have seen these benefits myself but couldn't summarize it like you do.

I have only written one benefit that is bring your styles to anywhere
devadi.netlify.app/unpolished?id=4...

 

thanks Aditya! yes agree, however I don't think this is a big selling point bc I find whenever I bring anything anywhere I end up doing a lot of rewriting anyway. in other words I think this benefit sounds bigger in theory than it really is in practice.

 

Tailwind is just CSS-in-HTML though...

 

This writing is excellent, thanks Shawn. Subtlely packed full of ideas that are deep-dives themselves.

My problem with Tailwind is that it's really a language. A language on top of two languages. And so I expect we'll have TailWind linters, TailWind re-formatters, TailWind toolchains, TailWind style guides, TailWind auto-completion... as a vetran of this industry, may I just say: SIGH :)

 

thank you so much!

yeah i hear you. that said, the alternative is really a custom "language" you cook up in your own team/company/projects, which is virtually guaranteed to have worse design, tooling, compatibility. people aren't just adopting this stuff just for the sake of it.

 

Awesome article! I also started using the CSS framework more and more lately. Even wrote a small guide on how to use Tailwind CSS with PostCSS and the configuration file. Many make the mistake thinking Tailwind only means having lots of classes for an element, which is not always true.

 

How would you relate it to CUBE CSS?

 

no idea. you take a crack at it?

 

I really like what I've seen from the philosophy behind CUBE CSS piccalil.li/blog/cube-css/. From what I can gather (and I don't trust my assessment, so recognize that these are unpolished musings), it seems to me that tailwind, which would fit into the "U" of CUBE, should fall behind the compositional CSS that CUBE CSS recommends as a first step.

This makes sense to me. You've pointed out some really good reasons why CSS-in-JS and module systems have challenges. I'm inclined to think that there's still a proper place for using CSS cascade as it is designed, even while using utility classes.

i took a quick look. i'd say you're being too superficial if you shoehorn tailwind into "U". the ultimate philosophy tailwind espouses is way more extreme - devolve all components into their most atomic elements and never bother with creating any classes or exceptions or composition selectors. this suits a much more fluid development style. i agree there is still a proper place for the cascade but it is likely <5% of the styles you write rather than 50% (for illustrative purposes just talkign about order of magnitude)

Thanks, I appreciate your thoughts on it. I don't have enough experience writing CSS to give the kind of evaluation that you just did, so that's very helpful.

 

Oh that BEM argument hits too close

 

dude it still haunts me. and for a long time I thought it was my fault for not getting it. now I realize I don't HAVE to give a shit about a flawed naming system.

 

This is really well written! It captures what I have thought and tried to say and puts it into better sentences. (icing.space/2019/the-css-spectrum-...)

One benefit missing like Aditya says is that it doesn’t lock you into a JavaScript-only world. Also I think the performance can be greater without the overheads of CSS-in-JS. It also makes server-rendering of React simple as you don’t have to capture the styles that were generated. I go into the performance implications here (I’m sure emotion is faster than styled-components here) icing.space/2019/pure-styling-of-c...

Plus I think you can do some pretty neat stuff around TypeScript and improving the developer experience of utility styles with type safety and autocompletion: icing.space/2019/pure-styling-of-c...

I agree that with Tailwind the setup could be simpler. I’m curious around your thoughts of the generated CSS file size and whether the purge options work well for you?

 

thanks! purge works fine. I don't see the benefit of TS for tailwind bc I rather use the Tailwind VS code extension rather than bloating my typescript toolchain with a zillion classes. and I mention but don't elaborate the performance aspect of not tying to JavaScript, but I believe that perf discussions without doing a benchmark on a realistic app are pointless.

 

Just how I think about it. Clear consistent future-proof naming, defined logic, easy to remove without breaking and transportable to future projects.

 

You can use the .cls button in chrome to design in browser with autocomplete and toggle buttons to turn classes on and off like this: twitter.com/calebporzio/status/102...

 

ah, cool, I did see that in someone's livestream!

 

btw i tried to illustrate these tradeoffs but failed horribly - pls feel free to take this and do something better excalidraw.com/#json=5756456242511...

 

That style-x though....

That is exactly what I want for cross platform web and mobile