DEV Community

loading...
Cover image for Why Tailwind CSS

Why Tailwind CSS

swyx
Infinite Builder 👷🏽‍♂️ I help people Learn in Public • Author, the Coding Career Handbook (https://learninpublic.org)
Originally published at swyx.io Updated on ・12 min read

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

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

Discussion (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 Author

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.

Thread Thread
mattwaler profile image
Matt Waler • Edited

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.

Thread Thread
swyx profile image
swyx Author • 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 Author

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

Thread Thread
mattwaler profile image
Matt Waler

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.

Thread Thread
swyx profile image
swyx Author

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

giorgosk profile image
Giorgos Kontopoulos 👀 • Edited

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.

Thread Thread
allsystemsarego profile image
ALLSYSTEMSAREGO • Edited

The unreleased Stylex library Facebook has developed referred to in the article above solves the Sublinear Scaling of CSS problem, without moving the problem to HTML like Tailwind appears to have done.

It identifies where the same styles sheet properties are reused across multiple components and generates the minimum number of classes nessesary.

This could probably be incorporated into the build process for tailwind.

< div className={stylex({
backgroundColor: 'blue',
color: 'black'
})} >

< div className={stylex({
backgroundColor: 'blue',
color: 'black',
fontWeight: 'bold'
})} >

Effectively renders to something like

.a {
backgroundColor: 'blue',
color: 'black'
}

.b {
fontWeight: 'bold'
}

< div className="a" >
< div className="a b" >

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 Author

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
zolidev 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
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 Author

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 Author

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 Author

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.

Collapse
ixartz profile image
Remi W.

Thank you for sharing your opinion about Tailwind CSS. I've also written a small blog post about Tailwind CSS and my thought at creativedesignsguru.com/why-i-love...

Collapse
silvenon profile image
Matija Marohnić • Edited

Great post! Very informative and much more relevant than the opposing one I read.

Regarding the opposing CSS variables point, I want to add that people can always configure Tailwind to use var(...) as color value, for example:

module.exports = {
  theme: {
    colors: {
      purple: "var(--color-purple)"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

So it's a non-issue.

Collapse
ryanhiebert profile image
Ryan Hiebert

How would you relate it to CUBE CSS?

Collapse
swyx profile image
swyx Author

no idea. you take a crack at it?

Collapse
ryanhiebert profile image
Ryan Hiebert

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.

Thread Thread
swyx profile image
swyx Author

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)

Thread Thread
ryanhiebert profile image
Ryan Hiebert

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.

Collapse
kamalhm profile image
Kamal

Oh that BEM argument hits too close

Collapse
swyx profile image
swyx Author

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 Author

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.

Collapse
j08438911 profile image
J

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

Collapse
swyx profile image
swyx Author

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

Collapse
swyx profile image
swyx Author • Edited

update: this is a normal part of my Tailwind workflow now, thank you for teaching me this :)

Collapse
tomhermans profile image
tom hermans

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

Collapse
beyondervn profile image
ngo hoang long

because it never goes wrong

Collapse
swyx profile image
swyx Author • Edited

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

Collapse
andthensumm profile image
Matt Marks 🐣

That style-x though....

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