DEV Community

Cover image for Why Tailwind CSS
swyx
swyx

Posted on • Edited on • Originally published at swyx.io

Why Tailwind CSS

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

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

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:

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:

  • 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 implemented in twind - the CSS in JS interpretation of tailwind!) Perhaps use of @apply is warranted, or just some smart typography. But overall I find that the ease of editing things inline overrides superficial aesthetic concerns. Users certainly don't care.
  • 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

Addressing Objections (Jan 2021 edit)

  • But the HTML is super ugly: code aesthetics should take a backseat to development velocity and ability to ship better UX.
  • But CSS Variables Exist!: yes, but you don't get the uninterrupted workflow of inline styles. margin-bottom: var(--spacing-8) is not equivalent to class="mb-8".
  • But Web Components Exist!: Of course you can use both together, but how much of your app is made up of web components, really? Tailwind is a great choice of base for those who are working on apps that aren't majority WC's.
  • I don't like @apply: Good, you're not supposed to use it too much anyway. But downleveling @apply to its constituent CSS is a trivial task.

Top comments (41)

Collapse
 
giorgosk profile image
Giorgos Kontopoulos 👀 • Edited

@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
Collapse
 
swyx profile image
swyx

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.

Collapse
 
giorgosk profile image
Giorgos Kontopoulos 👀 • Edited

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.

Collapse
 
nektro profile image
Comment marked as low quality/non-constructive by the community. View Code of Conduct
Meghan (she/her)

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.

Collapse
 
itaditya profile image
Aditya Agarwal

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

Collapse
 
swyx profile image
swyx

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.

Collapse
 
nektro profile image
Meghan (she/her)

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

Collapse
 
weakish profile image
Jang Rush

Hi, I've translated your svelet for sites, react for apps and enjoyed the translation process. I am asking your permission again, to translate this Tailwind CSS introduction to Chinese. Looking forward to your reply.

Collapse
 
swyx profile image
swyx

you always have my permission, just do it :)

Collapse
 
weakish profile image
Jang Rush

Chinese translation published: nextfe.com/why-tailwind-css/

BTW, it seems that Theme UI changed their url architecture. theme-ui.com/home/ is 404 now. The new url is theme-ui.com now.

Thread Thread
 
swyx profile image
swyx

thank you! will update

Collapse
 
bitwombat profile image
Bit Wombat

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

Collapse
 
swyx profile image
swyx

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.

 
swyx profile image
swyx • Edited

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.

Thread Thread
 
giorgosk profile image
Giorgos Kontopoulos 👀 • Edited

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

Thread Thread
 
swyx profile image
swyx

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

Thread Thread
 
giorgosk profile image
Giorgos Kontopoulos 👀

True gzip might be compressing and mitigating for all the repetition

Collapse
 
zoltanszogyenyi profile image
Zoltán Szőgyényi

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.

Collapse
 
echoes2099 profile image
echoes2099

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.

This seems like a big downside. CSS-in-JS has a very minimal learning curve since once you spin up a component, you just use regular CSS to style.

I guess what's not said is, "Why doesn't CSS-in-JS scale?"

Collapse
 
kamalhm profile image
Kamal

Oh that BEM argument hits too close

Collapse
 
swyx profile image
swyx

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.

Collapse
 
burntcaramel profile image
Patrick Smith

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?

Collapse
 
swyx profile image
swyx

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.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.