用工具类写样式
Building complex components from a constrained set of primitive utilities.
概述
用 Tailwind 写样式,就是把许多单一用途的表现类(工具类)直接组合在标记里:
You have a new message!
<!-- prettier-ignore -->
<div class="mx-auto flex max-w-sm items-center gap-x-4 rounded-xl bg-white p-6 shadow-lg outline outline-black/5 dark:bg-slate-800 dark:shadow-none dark:-outline-offset-1 dark:outline-white/10">
<img class="size-12 shrink-0" src="/img/logo.svg" alt="ChitChat Logo" />
<div>
<div class="text-xl font-medium text-black dark:text-white">ChitChat</div>
<p class="text-gray-500 dark:text-gray-400">You have a new message!</p>
</div>
</div>例如,上面的界面中我们用了:
- gap 工具类(
gap-x-4)处理 logo 与文字之间的间距
这种写样式的方式和许多传统最佳实践相悖,但一旦试过,你会很快注意到一些非常重要的好处:
- 完成得更快 — 不用花时间想类名、纠结选择器,也不用在 HTML 和 CSS 文件之间来回切换,设计会很快成型。
- 改动更安心 — 给某个元素添加或移除工具类只会影响该元素,不用担心意外破坏另一页里共用同一份 CSS 的东西。
- 维护旧项目更容易 — 改东西只需在项目里找到那个元素并改类名,而不用回忆半年没碰过的自定义 CSS 是怎么工作的。
- 代码更可移植 — 结构和样式都在同一处,你可以轻松复制粘贴整块 UI,甚至跨项目。
- CSS 不再膨胀 — 工具类高度可复用,CSS 不会随着每个新功能线性增长。
这些好处在小项目上已经很明显,对长期大规模协作的团队来说更有价值。
为什么不只用内联样式?
对这种方法的常见反应是:「这不就是内联样式吗?」某种程度上确实是——你是直接给元素应用样式,而不是先起个类名再给那个类写样式。
但使用工具类相比内联样式有许多重要优势,例如:
- 带约束地设计 — 用内联样式时,每个值都是魔法数字。用工具类时,你是从预定义的设计系统中选择样式,更容易做出视觉一致的 UI。
- 悬停、焦点和其他状态 — 内联样式无法针对 hover 或 focus 等状态,而 Tailwind 的状态变体让你能轻松用工具类为这些状态写样式。
- 媒体查询 — 内联样式里不能用媒体查询,但你可以用 Tailwind 的响应式变体轻松构建完全响应式的界面。
这个组件完全响应式,按钮带有 hover 和 active 样式,并且完全用工具类构建:

Erin Lindford
Product Engineer
<!-- [!code classes:sm:flex-row,sm:py-4,sm:gap-6,sm:mx-0,sm:shrink-0,sm:text-left,sm:items-center] -->
<!-- [!code classes:hover:text-white,hover:bg-purple-600,hover:border-transparent,active:bg-purple-700] -->
<div class="flex flex-col gap-2 p-8 sm:flex-row sm:items-center sm:gap-6 sm:py-4 ...">
<img class="mx-auto block h-24 rounded-full sm:mx-0 sm:shrink-0" src="/img/erin-lindford.jpg" alt="" />
<div class="space-y-2 text-center sm:text-left">
<div class="space-y-0.5">
<p class="text-lg font-semibold text-black">Erin Lindford</p>
<p class="font-medium text-gray-500">Product Engineer</p>
</div>
<!-- prettier-ignore -->
<button class="border-purple-200 text-purple-600 hover:border-transparent hover:bg-purple-600 hover:text-white active:bg-purple-700 ...">
Message
</button>
</div>
</div>用工具类思考
为悬停和焦点状态写样式
要为 hover 或 focus 等状态写样式,在任意工具类前加上要针对的状态前缀,例如 hover:bg-sky-700:
<!-- [!code word:hover\:bg-sky-700] -->
<button class="bg-sky-500 hover:bg-sky-700 ...">Save changes</button>这些前缀在 Tailwind 中称为变体,只有当变体条件匹配时,才会应用该工具类的样式。
hover:bg-sky-700 类生成的 CSS 如下:
.hover\:bg-sky-700 {
&:hover {
background-color: var(--color-sky-700);
}
}注意到这个类除非元素被悬停,否则什么都不做吗?它的唯一工作就是提供悬停样式——别无其他。
这和传统 CSS 的写法不同:传统上一个类通常会为多种状态提供样式:
<button class="btn">Save changes</button>
<style>
.btn {
background-color: var(--color-sky-500);
&:hover {
background-color: var(--color-sky-700);
}
}
</style>在 Tailwind 中还可以叠放变体,在多个条件同时匹配时应用工具类,例如组合 hover: 和 disabled:
<!-- [!code classes:disabled:hover:bg-sky-500] -->
<button class="bg-sky-500 disabled:hover:bg-sky-500 ...">Save changes</button>更多内容见悬停、焦点和其他状态文档。
媒体查询与断点
和悬停、焦点状态一样,你可以在任意工具类前加上希望生效的断点前缀,在不同断点为元素写样式:
<!-- [!code classes:sm:grid-cols-3] -->
<div class="grid grid-cols-2 sm:grid-cols-3">
<!-- ... -->
</div>上面的例子中,sm: 前缀确保 grid-cols-3 只在 sm 断点及以上触发,开箱即用时是 40rem:
.sm\:grid-cols-3 {
@media (width >= 40rem) {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}更多内容见响应式设计文档。
针对深色模式
为深色模式下的元素写样式,只需给希望在深色模式生效的工具类加上 dark: 前缀:
Light mode
Writes upside-down
The Zero Gravity Pen can be used to write in any orientation, including upside-down. It even works in outer space.
Dark mode
Writes upside-down
The Zero Gravity Pen can be used to write in any orientation, including upside-down. It even works in outer space.
<!-- [!code word:dark\:bg-gray-800] -->
<!-- prettier-ignore -->
<div class="bg-white dark:bg-gray-800 rounded-lg px-6 py-8 ring shadow-xl ring-gray-900/5">
<div>
<span class="inline-flex items-center justify-center rounded-md bg-indigo-500 p-2 shadow-lg">
<svg
class="h-6 w-6 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<!-- ... -->
</svg>
</span>
</div>
<!-- prettier-ignore -->
<!-- [!code word:dark\:text-white] -->
<h3 class="text-gray-900 dark:text-white mt-5 text-base font-medium tracking-tight ">Writes upside-down</h3>
<!-- prettier-ignore -->
<!-- [!code word:dark\:text-gray-400] -->
<p class="text-gray-500 dark:text-gray-400 mt-2 text-sm ">
The Zero Gravity Pen can be used to write in any orientation, including upside-down. It even works in outer space.
</p>
</div>和悬停状态或媒体查询一样,关键是要理解:单个工具类永远不会同时包含浅色和深色样式——你通过使用多个类来为深色模式写样式,一个管浅色,另一个管深色。
.dark\:bg-gray-800 {
@media (prefers-color-scheme: dark) {
background-color: var(--color-gray-800);
}
}更多内容见深色模式文档。
使用类组合
很多时候,用 Tailwind 甚至会用多个类来拼出单个 CSS 属性的值,例如给元素添加多个滤镜:
<!-- [!code classes:blur-sm,grayscale] -->
<div class="blur-sm grayscale">
<!-- ... -->
</div>这两种效果都依赖 CSS 的 filter 属性,因此 Tailwind 用 CSS 变量让这些效果可以组合在一起:
.blur-sm {
--tw-blur: blur(var(--blur-sm));
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-grayscale,);
}
.grayscale {
--tw-grayscale: grayscale(100%);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-grayscale,);
}上面生成的 CSS 略有简化,诀窍在于:每个工具类只为自己要应用的效果设置一个 CSS 变量。然后 filter 属性查看所有这些变量,如果变量未设置则回退为空。
使用任意值
Tailwind 中许多工具类由主题变量驱动,例如 bg-blue-500、text-xl 和 shadow-md,它们映射到底层调色板、字体比例和阴影。
当你需要使用主题之外的一次性值时,用特殊的方括号语法指定任意值:
<!-- [!code classes:bg-[#316ff6]] -->
<!-- prettier-ignore -->
<button class="bg-[#316ff6] ...">
Sign in with Facebook
</button>这对调色板之外的一次性颜色(如上例中的 Facebook 蓝)很有用,也适用于需要非常具体的网格这类复杂自定义值时:
<!-- [!code classes:grid-cols-[24rem_2.5rem_minmax(0,1fr)]] -->
<div class="grid grid-cols-[24rem_2.5rem_minmax(0,1fr)]">
<!-- ... -->
</div>即使你在使用主题值,需要用 calc() 这类 CSS 特性时也很有用:
<!-- [!code classes:max-h-[calc(100dvh-(--spacing(6)))]] -->
<div class="max-h-[calc(100dvh-(--spacing(6)))]">
<!-- ... -->
</div>甚至还有生成完全任意 CSS(包括任意属性名)的语法,这对设置 CSS 变量很有用:
<!-- [!code classes:[--gutter-width:1rem],lg:[--gutter-width:2rem]] -->
<div class="[--gutter-width:1rem] lg:[--gutter-width:2rem]">
<!-- ... -->
</div>更多内容见使用任意值文档。
这到底是怎么工作的?
Tailwind CSS 不像其他 CSS 框架那样是一份巨大的静态样式表——它在编译 CSS 时,根据你实际使用的类来生成所需的 CSS。
它会扫描项目中的所有文件,查找任何看起来可能是类名的符号:
// [!code word:px-4]
// [!code word:py-2]
// [!code word:rounded-md]
// [!code word:text-base]
// [!code word:px-5]
// [!code word:py-3]
// [!code word:rounded-lg]
// [!code word:text-lg]
// [!code word:font-bold]
export default function Button({ size, children }) {
let sizeClasses = {
md: "px-4 py-2 rounded-md text-base",
lg: "px-5 py-3 rounded-lg text-lg",
}[size];
return (
<button type="button" className={`font-bold ${sizeClasses}`}>
{children}
</button>
);
}找到所有潜在类名后,Tailwind 为每一个生成 CSS,并编译成一份只包含你实际需要的样式的样式表。
由于 CSS 是根据类名生成的,Tailwind 能识别 bg-[#316ff6] 这类使用任意值的类并生成所需 CSS,即使该值不在主题中。
更多工作原理见在源文件中检测类名。
复杂选择器
有时你需要在多种条件组合下为元素写样式,例如在深色模式、特定断点、悬停时,以及元素带有特定 data 属性时。
用 Tailwind 写出来是这样的:
<!-- [!code classes:dark:lg:data-current:hover:bg-indigo-600] -->
<button class="dark:lg:data-current:hover:bg-indigo-600 ...">
<!-- ... -->
</button>@media (prefers-color-scheme: dark) and (width >= 64rem) {
button[data-current]:hover {
background-color: var(--color-indigo-600);
}
}Tailwind 还支持 group-hover 这类写法,让你可以在特定父元素被悬停时为元素写样式:
<!-- [!code classes:group,group-hover:underline] -->
<a href="#" class="group rounded-lg p-8">
<!-- ... -->
<span class="group-hover:underline">Read more…</span>
</a>@media (hover: hover) {
a:hover span {
text-decoration-line: underline;
}
}这种 group-* 语法也适用于其他变体,例如 group-focus、group-active 以及更多。
对于非常复杂的场景(尤其是为你无法控制的 HTML 写样式时),Tailwind 支持任意变体,让你直接在类名里写任意选择器:
<!-- [!code classes:[&>[data-active]+span]:text-blue-600] -->
<div class="[&>[data-active]+span]:text-blue-600 ...">
<span data-active><!-- ... --></span>
<span>This text will be blue</span>
</div>div > [data-active] + span {
color: var(--color-blue-600);
}何时使用内联样式
在 Tailwind CSS 项目中,内联样式仍然非常有用,尤其是值来自数据库或 API 这类动态源时:
export function BrandedButton({ buttonColor, textColor, children }) {
return (
<button
style={{
backgroundColor: buttonColor,
color: textColor,
}}
className="rounded-md px-3 py-1.5 font-medium"
>
{children}
</button>
);
}对于格式化成类名后很难读的非常复杂的任意值,你也可能改用内联样式:
<!-- prettier-ignore -->
<div class="grid-[2fr_max(0,var(--gutter-width))_calc(var(--gutter-width)+10px)]">
<!-- prettier-ignore -->
<div style="grid-template-columns: 2fr max(0, var(--gutter-width)) calc(var(--gutter-width) + 10px)">
<!-- ... -->
</div>另一种有用的模式是用内联样式根据动态源设置 CSS 变量,再用工具类引用这些变量:
export function BrandedButton({ buttonColor, buttonColorHover, textColor, children }) {
return (
<button
style={{
"--bg-color": buttonColor,
"--bg-color-hover": buttonColorHover,
"--text-color": textColor,
}}
// [!code classes:bg-(--bg-color),text-(--text-color),hover:bg-(--bg-color-hover)]
className="bg-(--bg-color) text-(--text-color) hover:bg-(--bg-color-hover) ..."
>
{children}
</button>
);
}管理重复
当你完全用工具类构建整个项目时,难免会为了在不同地方复现同一设计而重复某些模式。
例如,这里每个头像图片的工具类重复了五次:
<!-- [!code classes:inline-block,h-12,w-12,rounded-full,ring-2,ring-white] -->
<div>
<div class="flex items-center space-x-2 text-base">
<h4 class="font-semibold text-slate-900">Contributors</h4>
<span class="bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-700 ...">204</span>
</div>
<div class="mt-3 flex -space-x-2 overflow-hidden">
<!-- prettier-ignore -->
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="https://images.unsplash.com/photo-1491528323818-fdd1faba62cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="" />
<!-- prettier-ignore -->
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="https://images.unsplash.com/photo-1550525811-e5869dd03032?ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="" />
<!-- prettier-ignore -->
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2.25&w=256&h=256&q=80" alt="" />
<!-- prettier-ignore -->
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="" />
<!-- prettier-ignore -->
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="https://images.unsplash.com/photo-1517365830460-955ce3ccd263?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="" />
</div>
<div class="mt-3 text-sm font-medium">
<a href="#" class="text-blue-500">+ 198 others</a>
</div>
</div>别慌!实践中这并不是你担心的那种问题,应对策略其实是你每天都在做的事。
使用循环
很多时候,渲染页面上出现多次的设计元素,实际只写了一次,因为真正的标记是在循环里渲染的。
例如,本指南开头那些重复的头像,在真实项目中几乎肯定会在循环里渲染:
<div>
<div class="flex items-center space-x-2 text-base">
<h4 class="font-semibold text-slate-900">Contributors</h4>
<span class="bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-700 ...">204</span>
</div>
<div class="mt-3 flex -space-x-2 overflow-hidden">
<!-- prettier-ignore -->
{#each contributors as user}
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src={user.avatarUrl} alt={user.handle} />
{/each}
</div>
<div class="mt-3 text-sm font-medium">
<a href="#" class="text-blue-500">+ 198 others</a>
</div>
</div>像这样在循环中渲染元素时,实际的类列表只写一次,所以并不存在需要解决的重复问题。
使用多光标编辑
当重复局限在单个文件里的一组元素时,最简单的处理方式是用多光标编辑,一次选中并编辑每个元素的类列表:
你会惊讶于这有多经常是最佳方案。如果能同时快速编辑所有重复的类列表,引入额外抽象并无好处。
使用组件
如果需要跨多个文件复用一些样式,最佳策略是:若使用 React、Svelte 或 Vue 这类前端框架,就创建一个组件;若使用 Blade、ERB、Twig 或 Nunjucks 这类模板语言,就创建一个模板片段。
export function VacationCard({ img, imgAlt, eyebrow, title, pricing, url }) {
return (
<div>
<img className="rounded-lg" src={img} alt={imgAlt} />
<div className="mt-4">
<div className="text-xs font-bold text-sky-500">{eyebrow}</div>
<div className="mt-1 font-bold text-gray-700">
<a href={url} className="hover:underline">
{title}
</a>
</div>
<div className="mt-2 text-sm text-gray-600">{pricing}</div>
</div>
</div>
);
}现在你可以在任意多处使用这个组件,同时样式仍有单一事实来源,可以在一处轻松一起更新。
使用自定义 CSS
如果用的是 ERB 或 Twig 这类模板语言,而不是 React 或 Vue,为一个按钮这么小的东西创建模板片段,相比一个简单的 btn CSS 类,可能会显得杀鸡用牛刀。
虽然强烈建议为更复杂的组件创建正规的模板片段,但当模板片段显得小题大做时,写一些自定义 CSS 完全没问题。
下面是一个 btn-primary 类可能的样子,使用主题变量保持设计一致:
<button class="btn-primary">Save changes</button>@import "tailwindcss";
@layer components {
.btn-primary {
border-radius: calc(infinity * 1px);
background-color: var(--color-violet-500);
padding-inline: --spacing(5);
padding-block: --spacing(2);
font-weight: var(--font-weight-semibold);
color: var(--color-white);
box-shadow: var(--shadow-md);
&:hover {
@media (hover: hover) {
background-color: var(--color-violet-700);
}
}
}
}不过再说一次,对于比单个 HTML 元素更复杂的东西,我们强烈建议使用模板片段,让样式和结构封装在同一处。
管理样式冲突
冲突的工具类
当你添加两个针对同一 CSS 属性的类时,样式表中后出现的类获胜。所以这个例子里,即使 flex 在实际 class 属性中排在后面,元素仍会得到 display: grid:
<!-- prettier-ignore -->
<div class="grid flex">
<!-- ... -->
</div>.flex {
display: flex;
}
.grid {
display: grid;
}一般来说,永远不要给同一个元素添加两个冲突的类——只添加你真正希望生效的那一个:
// [!code word:gridLayout\ \?\ \"grid\"\ \:\ \"flex\"]
export function Example({ gridLayout }) {
return <div className={gridLayout ? "grid" : "flex"}></div>;
}使用 React 或 Vue 这类基于组件的库时,这通常意味着为样式自定义暴露特定 props,而不是让使用者从组件外部追加额外的类,因为那些样式经常会冲突。
使用 important 修饰符
当你确实需要强制某个工具类生效、又没有其他办法管理优先级时,可以在类名末尾加上 !,让所有声明变成 !important:
<!-- [!code classes:bg-red-500!] -->
<!-- prettier-ignore -->
<div class="bg-teal-500 bg-red-500!">
<!-- ... -->
</div>/* [!code word:!important] */
.bg-red-500\! {
background-color: var(--color-red-500) !important;
}
.bg-teal-500 {
background-color: var(--color-teal-500);
}使用 important 标志
如果要把 Tailwind 加到已有高优先级复杂 CSS 的项目中,可以在导入 Tailwind 时使用 important 标志,把所有工具类标记为 !important:
/* [!code word:important] */
@import "tailwindcss" important;/* [!code word:!important] */
@layer utilities {
.flex {
display: flex !important;
}
.gap-4 {
gap: 1rem !important;
}
.underline {
text-decoration-line: underline !important;
}
}使用 prefix 选项
如果项目中的类名与 Tailwind CSS 工具类冲突,可以用 prefix 选项给所有 Tailwind 生成的类和 CSS 变量加上前缀:
/* [!code word:important] */
@import "tailwindcss" prefix(tw);/* [!code word:tw\:] */
@layer theme {
:root {
--tw-color-red-500: oklch(0.637 0.237 25.331);
}
}
@layer utilities {
.tw\:text-red-500 {
color: var(--tw-color-red-500);
}
}