Tailwind is a Leaky Abstraction
I have to admit: as I’ve watched Tailwind enthusiastically adopted by more and more of the frontend community, I’ve remained skeptical. But, having never used it, I decided to keep quiet until I had an informed opinion.
我必须承认:当我看到 Tailwind 被越来越多的前端社区热情采用时,我仍然持怀疑态度。但是,由于从未使用过它,我决定保持沉默,直到得到明智的意见。
Well, I’ve spent the past few months at work learning Tailwind with an open mind. I can now confidently say that I do, in fact, dislike Tailwind, and I wouldn’t use it for any new projects.
嗯,过去几个月我在工作中以开放的心态学习 Tailwind。我现在可以自信地说,事实上,我确实不喜欢 Tailwind,而且我不会将它用于任何新项目。
Tailwind is commonly described as “utility classes”, but that’s a bit of an understatement. It’s essentially a small language you write in the class attributes of your HTML that compiles to a combination of CSS rules and selectors — an abstraction over CSS. But all abstractions leak, and Tailwind is very leaky.
Tailwind 通常被描述为“实用类”,但这有点轻描淡写。它本质上是一种在 HTML 的类属性中编写的小语言,可编译为 CSS 规则和选择器的组合——CSS 的抽象。但所有抽象都会泄漏,Tailwind 的泄漏非常严重。
Let’s take transforms. I was trying to build an animation in which an element rotates in 3D space, so that it appears to come toward the viewer. To achieve that, I needed to rotate the element around the X axis. With CSS, this is easy: set a perspective
on the parent element, and then set transform: rotateX
on the element you want to animate:
让我们进行变换。我试图构建一个动画,其中元素在 3D 空间中旋转,使其看起来朝向观看者。为了实现这一点,我需要绕 X 轴旋转元素。使用 CSS,这很简单:在父元素上设置 perspective
,然后在要设置动画的元素上设置 transform: rotateX
:
.parent {
perspective: 1000px;
}
.animate {
transform: rotateX(45deg);
}
Unfortunately, Tailwind doesn’t support the perspective
property. Implementing any sort of 3D design requires breaking out of Tailwind.
不幸的是,Tailwind 不支持 perspective
属性。实施任何类型的 3D 设计都需要摆脱 Tailwind。
Not that it would have mattered, though. Tailwind only supports the unadorned rotate
, which uses the Z axis. If you want to rotate along any other axis, you’re out of luck. A discussion has been ongoing for almost two years, with the official recommendation being to just write a custom JavaScript plugin to implement it.
不过,这并不重要。 Tailwind 仅支持朴素的 rotate
,它使用 Z 轴。如果你想沿着任何其他轴旋转,那你就不走运了。讨论已经持续了近两年,官方建议是编写一个自定义 JavaScript 插件来实现它。
You might protest that this is a niche issue (that’s what the Tailwind team says in the discussion). But there are issues with utilities that you’re likely to encounter in regular usage, too.
您可能会抗议说这是一个小众问题(这是 Tailwind 团队在讨论中所说的)。但是,您在日常使用中也可能会遇到实用程序的问题。
Even though it’s one of the main jobs of CSS, spacing things out has always been kind of a pain. Maybe that’s why Tailwind introduced the Space Between utilities. Just throw space-x-2
or some similar class on the parent, and the children will magically work themselves out!
尽管这是 CSS 的主要工作之一,但将事物分开一直是一种痛苦。也许这就是 Tailwind 推出 Space Between 实用程序的原因。只需向父级扔 space-x-2
或类似的类,孩子们就会神奇地自行解决!
Of course, it’s not magic. What it actually does is use child and sibling selectors to set margins on the child elements. The aforelinked page shows the generated CSS:
当然,这不是魔法。它实际上所做的是使用子选择器和同级选择器来设置子元素的边距。上述链接页面显示了生成的 CSS:
.space-x-2 > * + * {
margin-left: 0.5rem;
}
That applies a margin-left
to every child after the first of an element with the class space-x-2
. The relevant consequence is that now, attempting to set a left margin using a selector with lower specificity — say, the selector generated by a Tailwind margin utility — will fail.
这会将 margin-left
应用于类 space-x-2
的第一个元素之后的每个子元素。相关的后果是,现在尝试使用具有较低特异性的选择器(例如由 Tailwind 边距实用程序生成的选择器)设置左边距将会失败。
I ran into this issue when I was doing exactly that: trying to use a margin utility on an element whose parent already had one of these space utilities applied to it. It took me a while to realize why every margin I set seemed to get ignored. These features are mutually exclusive.
当我这样做时,我遇到了这个问题:尝试在其父级已经应用了这些空间实用程序之一的元素上使用边距实用程序。我花了一段时间才意识到为什么我设置的每一个边距似乎都被忽略了。这些功能是互斥的。
In the meantime, CSS has gotten much better at spacing! You can accomplish the exact same thing without Tailwind by using two properties:
与此同时,CSS 在间距方面已经变得更好了!您可以使用两个属性在没有 Tailwind 的情况下完成完全相同的事情:
display: flex;
column-gap: 0.5rem;
Originally, I was going to write about how Tailwind doesn’t support attribute selectors. But a few days before I started this post, they came out with a huge update that added them. It’s a big improvement, but it also includes some of the leakiest parts of the abstraction yet. I’m referring in particular to the “arbitrary variants” feature that allows you to include CSS selectors in your Tailwind classes.
最初,我打算写一下 Tailwind 如何不支持属性选择器。但在我开始写这篇文章的前几天,他们发布了一个巨大的更新,添加了它们。这是一个很大的改进,但它也包括一些抽象中最容易泄漏的部分。我特别指的是“任意变体”功能,该功能允许您在 Tailwind 类中包含 CSS 选择器。
Let’s look at some of the selectors here:
让我们看看这里的一些选择器:
<div class="[&_p]:mt-4">
<!-- ... -->
</div>
The &
sigil lets you control nesting, similar to Sass and (soon) plain CSS. In CSS, the selector would be & p
. Tailwind requires underscores instead of spaces, though, because a space would split the class name in two.
&
印记可以让你控制嵌套,类似于 Sass 和(很快)纯 CSS。在 CSS 中,选择器是 & p
。不过,Tailwind 需要下划线而不是空格,因为空格会将类名分成两部分。
Here’s something a little more complex:
这里有一些更复杂的事情:
<ul role="list">
{#each items as item}
<li class="lg:[&:nth-child(3)]:hover:underline">{item}</li>
{/each}
</ul>
To understand this class, you need to know how nth-child
works. That’s an abstraction leak. But worse than that, it highlights a key shortcoming of Tailwind. In CSS, to set multiple properties using the same selector, you can write it once and group all the properties together. In Tailwind, there’s no choice but to write the full query again for every property:
要理解这个类,您需要知道 nth-child
是如何工作的。这是一个抽象泄漏。但更糟糕的是,它凸显了 Tailwind 的一个关键缺点。在 CSS 中,要使用同一选择器设置多个属性,您可以编写一次并将所有属性分组在一起。在 Tailwind 中,别无选择,只能为每个属性再次编写完整的查询:
<ul role="list">
{#each items as item}
<li
class="lg:[&:nth-child(3)]:hover:underline lg:[&:nth-child(3)]:hover:font-bold lg:[&:nth-child(3)]:hover:text-blue-600 lg:[&:nth-child(3)]:hover:opacity-100"
>
{item}
</li>
{/each}
</ul>
Notice the long horizontal scrollbar. This isn’t just an abstraction that leaks some of the underlying details. It’s an abstraction that actively makes the experience worse.
注意长水平滚动条。这不仅仅是一个泄露了一些底层细节的抽象。这种抽象会导致体验变得更糟。
Meanwhile, here’s the CSS that would be needed to accomplish the same thing. It’s still not simple. But it is scannable, and it doesn’t repeat the entire selector four different times.
同时,这是完成同样的事情所需的 CSS。这仍然不简单。但它是可扫描的,并且不会将整个选择器重复四次。
@media (min-width: 1024px) {
li:nth-child(3):hover {
text-decoration: underline;
font-weight: bold;
color: var(--blue-600);
opacity: 1;
}
}
I realize this is a bit of a catch-22. Before, I was criticizing Tailwind for hiding CSS internals; now I’m criticizing it for exposing them. But that’s kind of the point. Tailwind is a layer on top of CSS, but it doesn’t actually hide any complexity in the layer below. You still need to know CSS.
我意识到这有点像第 22 条军规。之前,我批评 Tailwind 隐藏了 CSS 内部结构;现在我批评它揭露了他们。但这就是重点。 Tailwind 是 CSS 之上的一层,但它实际上并没有隐藏下面一层的任何复杂性。你仍然需要了解 CSS。
This might be unfair to Tailwind. To my knowledge, the team has never promoted it as a CSS replacement. At its core, it really is just a set of class names that apply styles. But even after working with it for months, there’s still a mental translation layer between “Tailwind CSS” and “real CSS”.
这对 Tailwind 来说可能不公平。据我所知,该团队从未将其作为 CSS 替代品进行推广。从本质上讲,它实际上只是一组应用样式的类名。但即使在使用了几个月之后,“Tailwind CSS”和“真正的 CSS”之间仍然存在一个心理翻译层。
These issues don’t mean Tailwind is bad. They’re just some of the tradeoffs you inevitably encounter when using a tool.
这些问题并不意味着 Tailwind 不好。它们只是您在使用工具时不可避免地遇到的一些权衡。
Maybe you really want to avoid coming up with names. Maybe you want to see your styles inside your markup, or use a ready-made set of design tokens. Tailwind will let you do those things. But the price you pay is that you need to know exactly how this tool interacts with CSS.
也许你真的想避免说出名字。也许您想在标记中查看样式,或者使用一组现成的设计标记。 Tailwind 会让你做这些事情。但你付出的代价是你需要确切地知道这个工具如何与 CSS 交互。
To me, that trade is a dealbreaker. It increases the number of tools I use without really giving me anything in return. It’s so leaky that I have to constantly think about the thing it’s supposed to abstract away. Yes, it’s nice to have my styles in the same file as the rest of my component code. But that convenience is heavily outweighed by the overhead of actually using Tailwind.
对我来说,这笔交易是一个破坏性的交易。它增加了我使用的工具数量,却没有真正给我任何回报。它是如此的漏洞,以至于我必须不断地思考它应该抽象掉的东西。是的,很高兴将我的样式与组件代码的其余部分放在同一个文件中。但实际使用 Tailwind 的开销远远超过了这种便利性。