跳到主要内容

Tailwind 设计理念与对比

TL;DR(更像实战笔记)

  • Tailwind 把「设计 token → 类名 → 插件 → 产物」做成闭环,加上 tailwind-merge/cva/tailwind-variants/IDE 提示,默认路就很宽;如果不想自己养 preset/merge 规则,就选它。
  • UnoCSS 灵活得像乐高:任意规则、任意 preset,但 merge/类型提示要自己兜底,团队需要有人养规则库;适合愿意自定义、且已有内部 preset 的团队。

核心特性速览(顺便说说坑)

  • JIT + content 精准扫描:只生成用到的类。content 太宽(如 ./src/**/* 加上动态字符串)会让产物膨胀,建议精确到模板目录;SSR/RSC 场景需保证 server 侧也能扫描到。
  • 分层@layer base/components/utilities 控制覆盖优先级。@apply 可用,但别跨层堆叠;出现冲突优先回到 variants 工厂解决。
  • variantsmd:hover:group-/peer-aria-data-,以及自定义 variant。关系类很强大,但如果嵌套过多,可读性会崩,最好有 lint/评审清单。
  • Design token 对齐:把色板、间距、圆角写成 tokens(CSS 变量或 @theme),类名只引用语义,不直接写 #fff。切换品牌/暗色时,只改 tokens。
  • 插件与生态:typography/forms/line-clamp/animate 等官方插件,加上 shadcn/ui、reka-ui 的大量范式与可复制代码。选 Tailwind 的核心理由之一是「社区资产可直接拿来」。

Tailwind vs UnoCSS(决策表)

维度TailwindUnoCSS
生态/社区大量预设、主题、范式示例、tailwind-merge 成熟灵活 rule/variant 扩展,预设较少
类型提示@tailwindcss/language-service/IDE 支持完善需额外插件,提示程度取决于 preset
Merge 去重tailwind-merge 针对 Tailwind 语义(含 v4)有完整规则unocss 默认不处理 class 去重,需要自定规则
运行时v4 JIT 产物稳定,适配 RSC/SSR即时模式极快,支持极度自定义;但团队需维护自定义 preset
迁移/资产现成组件库、范式、设计体系丰富灵活性高,迁移成本取决于自建规则

什么时候更适合 Tailwind?

  • 团队不想养一套 merge/variant 规则,也不想自己维护 IDE 提示;更看重「拿来就能跑」。
  • 需要快速对齐社区资产(shadcn/ui、reka-ui 组件、插件、设计体系),或者要让外部协作成员快速上手。
  • 需要与 RSC/SSR、Next/Vite/Rspack 等框架顺畅集成,且希望默认行为稳定(例如 preflight、分层、JIT 产物)。

什么时候会选 UnoCSS?

  • 需要大量自定义规则(如内部 DSL),或希望把类名语义定得比 Tailwind 更短、更贴近业务。
  • 产物必须极小、规则必须可控(例如小程序/低码引擎内嵌),团队有人维护 preset。
  • 需要更多运行时调度(如按需 rule 生成)且能接受自己维护 merge/类型提示。

tailwind-merge、cva、tailwind-variants 思想

tailwind-merge

  • 作用:在运行时/构建时去重与解决冲突(如 p-4 + p-2p-2)。
  • 使用建议:将 clsx + tailwind-merge 包装为 cn,用于所有 class 拼接;对自定义主题添加自定义规则时同步更新 merge config。遇到边缘 case(如自定义色板/大小),要验证 merge 是否按预期覆盖。

class-variance-authority (cva)

  • 思路:集中声明 variantsdefaultVariantscompoundVariants,输出 class builder,保持样式与状态的单一真实来源。
  • 示例:
import { cva } from 'class-variance-authority'

export const badgeVariants = cva('inline-flex items-center rounded-md text-xs font-medium', {
variants: {
tone: {
subtle: 'bg-muted text-muted-foreground border border-border',
brand: 'bg-primary text-primary-foreground',
danger: 'bg-destructive text-white',
},
size: {
sm: 'px-2 py-1',
md: 'px-2.5 py-1.5',
},
},
compoundVariants: [{ tone: 'brand', size: 'md', class: 'shadow-sm' }],
defaultVariants: { tone: 'subtle', size: 'md' },
})

tailwind-variants (tv)

  • 思路:在 cva 的基础上内置 tailwind-merge,提供 slots/recipes、更严格的类型推导;适合大型设计系统。
  • 示例:
import { tv } from 'tailwind-variants'

export const input = tv({
base: 'inline-flex h-10 w-full rounded-md border bg-background px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring',
variants: {
state: { default: '', invalid: 'border-destructive ring-destructive/30' },
density: { cozy: 'py-2', compact: 'py-1.5 text-xs' },
},
defaultVariants: { state: 'default', density: 'cozy' },
})

// 调用:input({ state: 'invalid', class: 'w-64' })

决策:需要 slots、自动 merge、强类型时选 tailwind-variants;仅需轻量 variants 时 cva 即可。

实战对比:cva vs tv

  • cva:轻量、心智负担低。适合按钮、Badge 这类单槽组件;合并需要 cn 包一层。
  • tv:内置 merge,提供 slots/recipes,能把组件拆成多个 slot 并保持类型安全;适合卡片、模态框等多槽位组件。成本是心智模型更复杂,但大型设计系统更省力。

RSC/SSR/HMR 下的注意点(踩坑后总结)

  • RSC(React Server Components):确保 content 能扫描到 server 组件模板;避免在服务器拼接动态类名;tailwind-merge/cva 可安全用于 server 端。
  • SSR:Tailwind v4 产物可直接 SSR;注意 preflight 是否与宿主冲突,必要时局部关闭或按 route 注入。
  • HMR:JIT 很快,但自定义插件过多会拖慢冷启动;遇到慢启动时先收紧 content,再检查插件链。

preflight 与 @apply 注意事项

  • preflight 会重置全局样式:在组件库/微前端场景要确认不与宿主冲突,必要时局部关闭。
  • @apply 适用于提炼重复类,但避免跨层引用或堆叠太多原子类;当出现冲突时优先回到 cva/tv 的 variants 声明。