Why I Love Tailwind
Dec 07, 2020
46,315 views
Tailwind is an atomic CSS framework that has taken the frontend world by storm. It gives developers without a deep understanding of design the ability to build visually gorgeous, modern user interfaces.
If you have not seen it before, here is the canonical Tailwind example from their original homepage:
<div class="shadow-lg flex bg-white rounded-lg p-6 leading-normal">
<img class="h-24 w-24 rounded-full mx-0 mr-6" src="avatar.jpg" />
<div class="text-left">
<h1 class="text-lg">Erin Lindford</h1>
<h2 class="text-purple-500">Customer Support</h2>
<p class="text-slate-600">erinlindford@example.com</p>
<p class="text-slate-600">(555) 765-4321</p>
</div>
</div>
Many people think Tailwind is cool because it uses atomic CSS. Here is the thing though: Tailwind is awesome despite using atomic CSS, not because of it.
Hear me out.
The key to Tailwind
We have had atomic CSS frameworks for almost a decade but none of them have been as critically acclaimed as Tailwind. What makes it different?
The key to Tailwind’s popularity is the painstakingly constructed system of design tokens at the core of the framework. The system’s carefully selected constraints give developers just the right guardrails. They make it obvious whether a choice is good or bad by offering only discrete steps.
This does require some design taste, but most frontend engineers I know have developed that over the years of building user interfaces. Tailwind’s system lets them turn that taste into implementation without requiring a lot of design skill — it helps them cross “the gap”.
Tailwind’s system is a masterpiece of design. I, and many other developers all around the world, feel empowered by and love it.
The problem with Tailwind
The atomic CSS framework is basically a delivery mechanism that allows developers to apply the system to their UIs. It’s undeniable that it has a fantastic developer experience: once you get used to the custom vocabulary you feel like you are flying!
However, we have learned over the past decade that atomic CSS has downsides:
- Users still have to add a separate setup for the custom CSS they inevitably need (coined “bailwind”). You cannot get by on just Tailwind in the real world. Not having a dedicated place for custom styles in the same system can cause maintenance issues down the line.
- Due to file-size considerations, Tailwind does not include all variants (e.g.
hover:
,sm:
) for all utilities by default. It leaves it to you to manually configure which ones you need for every single CSS property. - Atomic CSS is not ideal for performance. No tooling can extract the per-page critical CSS, so you end up shipping more CSS to the browser than necessary. The bigger and more dynamic the app, the more unnecessary code you will ship.[^1]
Tailwind without the downsides
Brent Jackson, the creator of one of the original atomic CSS libraries, said it best in his post on atomic CSS:
“This methodology was created before React was released and was intended for use in template-based user interfaces, including Rails and PHP. It was never designed for functional component-based UI and doesn’t take advantage of this new paradigm.”
Now, here is the thing: you can have your cake and eat it too. You can use Tailwind’s marvelous system and fantastic developer experience without the downsides of atomic CSS.
How? twin.macro.
Let me illustrate. Here is the canonical Tailwind example built with twin.macro and React:
import 'twin.macro'
const Card = () => (
<div tw="shadow-lg md:flex bg-white rounded-lg p-6 leading-normal">
<img
tw="h-16 w-16 md:h-24 md:w-24 rounded-full mx-auto md:mx-0 md:mr-6"
src="avatar.jpg"
/>
<div tw="text-center md:text-left">
<h1 tw="text-lg">Erin Lindford</h1>
<h2 tw="text-purple-500">Customer Support</h2>
<p tw="text-slate-600">erinlindford@example.com</p>
<p tw="text-slate-600">(555) 765-4321</p>
</div>
</div>
)
Unsurprisingly, the result looks identical — we are still using the same system after all. Even the code looks the same, except that we use the tw
prop instead of the class
attribute!
However, under the hood this automatically compiles the class names to the actual CSS they refer to (with the css prop):
import 'twin.macro'
;<div tw="text-center md:text-left" />
// ↓↓↓↓↓ turns into ↓↓↓↓↓
import 'styled-components/macro'
;<div
css={{
textAlign: 'center',
'@media (min-width: 768px)': {
textAlign: 'left',
},
}}
/>
You get to use Tailwind’s system and developer experience and take advantage of all the benefits of CSS-in-JS:
-
Extending your elements with custom styles is as simple as using the css prop, no extra separate setup required to “bailwind”:
import "twin.macro"
<div
tw="text-center md:text-left"
css={{`
&:hover {
background-image: url("/bg.png");
}
`}}
/> -
You can use all variants in all combinations with all utilities allowing for even more expression within the system. Since twin.macro runs at build-time, you don’t have to worry about configuration or file size:
import 'twin.macro'
;<div tw="sm:hover:first:bg-black">...</div> -
You get fully automatic critical CSS extraction and code splitting. Users will only load exactly the styles they need for the page they requested — nothing more and nothing less! CSS performance does not get better.[^1]
The ideal setup for both developer and user experience!
Rather than taking two steps forward with Tailwind’s system and one step backward with atomic CSS, let’s take five steps forward. Together.
[^1]: CSS-in-JS automatically extracts the critical CSS for the requested page and inlines it into a tag. That means the first-paint with CSS-in-JS will always be faster as it saves both an extra network request for the
.css
file as well as sending less CSS code to the client. While the Time To Interactive for small apps will be slightly slower (as the JavaScript bundle includes the CSS-in-JS library you use) once your app grows the network request for the larger CSS file can outweigh any CSS-in-JS library and can cause a slower Time To Interactive as well.