Skip to content
全部文档

用工具类写样式

Building complex components from a constrained set of primitive utilities.

概述

用 Tailwind 写样式,就是把许多单一用途的表现类(工具类)直接组合在标记里:

ChitChat

You have a new message!

html
<!-- 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>

例如,上面的界面中我们用了:

这种写样式的方式和许多传统最佳实践相悖,但一旦试过,你会很快注意到一些非常重要的好处:

  • 完成得更快 — 不用花时间想类名、纠结选择器,也不用在 HTML 和 CSS 文件之间来回切换,设计会很快成型。
  • 改动更安心 — 给某个元素添加或移除工具类只会影响该元素,不用担心意外破坏另一页里共用同一份 CSS 的东西。
  • 维护旧项目更容易 — 改东西只需在项目里找到那个元素并改类名,而不用回忆半年没碰过的自定义 CSS 是怎么工作的。
  • 代码更可移植 — 结构和样式都在同一处,你可以轻松复制粘贴整块 UI,甚至跨项目。
  • CSS 不再膨胀 — 工具类高度可复用,CSS 不会随着每个新功能线性增长。

这些好处在小项目上已经很明显,对长期大规模协作的团队来说更有价值。

为什么不只用内联样式?

对这种方法的常见反应是:「这不就是内联样式吗?」某种程度上确实是——你是直接给元素应用样式,而不是先起个类名再给那个类写样式。

但使用工具类相比内联样式有许多重要优势,例如:

  • 带约束地设计 — 用内联样式时,每个值都是魔法数字。用工具类时,你是从预定义的设计系统中选择样式,更容易做出视觉一致的 UI。
  • 悬停、焦点和其他状态 — 内联样式无法针对 hover 或 focus 等状态,而 Tailwind 的状态变体让你能轻松用工具类为这些状态写样式。
  • 媒体查询 — 内联样式里不能用媒体查询,但你可以用 Tailwind 的响应式变体轻松构建完全响应式的界面。

这个组件完全响应式,按钮带有 hover 和 active 样式,并且完全用工具类构建:

Woman's Face

Erin Lindford

Product Engineer

html
<!-- [!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

html
<!-- [!code word:hover\:bg-sky-700] -->
<button class="bg-sky-500 hover:bg-sky-700 ...">Save changes</button>

这些前缀在 Tailwind 中称为变体,只有当变体条件匹配时,才会应用该工具类的样式。

hover:bg-sky-700 类生成的 CSS 如下:

Generated CSS
css
.hover\:bg-sky-700 {
  &:hover {
    background-color: var(--color-sky-700);
  }
}

注意到这个类除非元素被悬停,否则什么都不做吗?它的唯一工作就是提供悬停样式——别无其他。

这和传统 CSS 的写法不同:传统上一个类通常会为多种状态提供样式:

HTML
html
<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:

html
<!-- [!code classes:disabled:hover:bg-sky-500] -->
<button class="bg-sky-500 disabled:hover:bg-sky-500 ...">Save changes</button>

更多内容见悬停、焦点和其他状态文档。

媒体查询与断点

和悬停、焦点状态一样,你可以在任意工具类前加上希望生效的断点前缀,在不同断点为元素写样式:

01
02
03
04
05
06
html
<!-- [!code classes:sm:grid-cols-3] -->
<div class="grid grid-cols-2 sm:grid-cols-3">
  <!-- ... -->
</div>

上面的例子中,sm: 前缀确保 grid-cols-3 只在 sm 断点及以上触发,开箱即用时是 40rem:

Generated CSS
css
.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.

html
<!-- [!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>

和悬停状态或媒体查询一样,关键是要理解:单个工具类永远不会同时包含浅色和深色样式——你通过使用多个类来为深色模式写样式,一个管浅色,另一个管深色。

Generated CSS
css
.dark\:bg-gray-800 {
  @media (prefers-color-scheme: dark) {
    background-color: var(--color-gray-800);
  }
}

更多内容见深色模式文档。

使用类组合

很多时候,用 Tailwind 甚至会用多个类来拼出单个 CSS 属性的值,例如给元素添加多个滤镜:

HTML
html
<!-- [!code classes:blur-sm,grayscale] -->
<div class="blur-sm grayscale">
  <!-- ... -->
</div>

这两种效果都依赖 CSS 的 filter 属性,因此 Tailwind 用 CSS 变量让这些效果可以组合在一起:

Generated CSS
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 对渐变阴影颜色变换等也用同样的方法。

使用任意值

Tailwind 中许多工具类由主题变量驱动,例如 bg-blue-500text-xlshadow-md,它们映射到底层调色板、字体比例和阴影。

当你需要使用主题之外的一次性值时,用特殊的方括号语法指定任意值:

HTML
html
<!-- [!code classes:bg-[#316ff6]] -->
<!-- prettier-ignore -->
<button class="bg-[#316ff6] ...">
  Sign in with Facebook
</button>

这对调色板之外的一次性颜色(如上例中的 Facebook 蓝)很有用,也适用于需要非常具体的网格这类复杂自定义值时:

HTML
html
<!-- [!code classes:grid-cols-[24rem_2.5rem_minmax(0,1fr)]] -->
<div class="grid grid-cols-[24rem_2.5rem_minmax(0,1fr)]">
  <!-- ... -->
</div>

即使你在使用主题值,需要用 calc() 这类 CSS 特性时也很有用:

HTML
html
<!-- [!code classes:max-h-[calc(100dvh-(--spacing(6)))]] -->
<div class="max-h-[calc(100dvh-(--spacing(6)))]">
  <!-- ... -->
</div>

甚至还有生成完全任意 CSS(包括任意属性名)的语法,这对设置 CSS 变量很有用:

HTML
html
<!-- [!code classes:[--gutter-width:1rem],lg:[--gutter-width:2rem]] -->
<div class="[--gutter-width:1rem] lg:[--gutter-width:2rem]">
  <!-- ... -->
</div>

更多内容见使用任意值文档。

这到底是怎么工作的?

Tailwind CSS 不像其他 CSS 框架那样是一份巨大的静态样式表——它在编译 CSS 时,根据你实际使用的类来生成所需的 CSS。

它会扫描项目中的所有文件,查找任何看起来可能是类名的符号:

Button.jsx
jsx
// [!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 写出来是这样的:

HTML
html
<!-- [!code classes:dark:lg:data-current:hover:bg-indigo-600] -->
<button class="dark:lg:data-current:hover:bg-indigo-600 ...">
  <!-- ... -->
</button>
Simplified CSS
css
@media (prefers-color-scheme: dark) and (width >= 64rem) {
  button[data-current]:hover {
    background-color: var(--color-indigo-600);
  }
}

Tailwind 还支持 group-hover 这类写法,让你可以在特定父元素被悬停时为元素写样式:

HTML
html
<!-- [!code classes:group,group-hover:underline] -->
<a href="#" class="group rounded-lg p-8">
  <!-- ... -->
  <span class="group-hover:underline">Read more…</span>
</a>
Simplified CSS
css
@media (hover: hover) {
  a:hover span {
    text-decoration-line: underline;
  }
}

这种 group-* 语法也适用于其他变体,例如 group-focusgroup-active 以及更多

对于非常复杂的场景(尤其是为你无法控制的 HTML 写样式时),Tailwind 支持任意变体,让你直接在类名里写任意选择器:

HTML
html
<!-- [!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>
Simplified CSS
css
div > [data-active] + span {
  color: var(--color-blue-600);
}

何时使用内联样式

在 Tailwind CSS 项目中,内联样式仍然非常有用,尤其是值来自数据库或 API 这类动态源时:

branded-button.jsx
jsx
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>
  );
}

对于格式化成类名后很难读的非常复杂的任意值,你也可能改用内联样式:

HTML
html
<!-- 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 变量,再用工具类引用这些变量:

branded-button.jsx
jsx
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>
  );
}

管理重复

当你完全用工具类构建整个项目时,难免会为了在不同地方复现同一设计而重复某些模式。

例如,这里每个头像图片的工具类重复了五次:

Contributors

204
html
<!-- [!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>

别慌!实践中这并不是你担心的那种问题,应对策略其实是你每天都在做的事。

使用循环

很多时候,渲染页面上出现多次的设计元素,实际只写了一次,因为真正的标记是在循环里渲染的。

例如,本指南开头那些重复的头像,在真实项目中几乎肯定会在循环里渲染:

svelte
<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 这类模板语言,就创建一个模板片段

jsx
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 类可能的样子,使用主题变量保持设计一致:

HTML
html
<button class="btn-primary">Save changes</button>
CSS
css
@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

HTML
html
<!-- prettier-ignore -->
<div class="grid flex">
  <!-- ... -->
</div>
CSS
css
.flex {
  display: flex;
}
.grid {
  display: grid;
}

一般来说,永远不要给同一个元素添加两个冲突的类——只添加你真正希望生效的那一个:

example.jsx
jsx
// [!code word:gridLayout\ \?\ \"grid\"\ \:\ \"flex\"]
export function Example({ gridLayout }) {
  return <div className={gridLayout ? "grid" : "flex"}></div>;
}

使用 React 或 Vue 这类基于组件的库时,这通常意味着为样式自定义暴露特定 props,而不是让使用者从组件外部追加额外的类,因为那些样式经常会冲突。

使用 important 修饰符

当你确实需要强制某个工具类生效、又没有其他办法管理优先级时,可以在类名末尾加上 !,让所有声明变成 !important

HTML
html
<!-- [!code classes:bg-red-500!] -->
<!-- prettier-ignore -->
<div class="bg-teal-500 bg-red-500!">
  <!-- ... -->
</div>
Generated CSS
css
/* [!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

app.css
css
/* [!code word:important] */
@import "tailwindcss" important;
Compiled CSS
css
/* [!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 变量加上前缀:

app.css
css
/* [!code word:important] */
@import "tailwindcss" prefix(tw);
Compiled CSS
css
/* [!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);
  }
}

文档译文以 MIT 协议授权;原文版权归 Tailwind Labs。湘ICP备2026005453号-2