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 工厂解决。 - variants:
md:、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(决策表)
| 维度 | Tailwind | UnoCSS |
|---|---|---|
| 生态/社区 | 大量预设、主题、范式示例、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-2→p-2)。 - 使用建议:将
clsx+tailwind-merge包装为cn,用于所有 class 拼接;对自定义主题添加自定义规则时同步更新 merge config。遇到边缘 case(如自定义色板/大小),要验证 merge 是否按预期覆盖。
class-variance-authority (cva)
- 思路:集中声明
variants、defaultVariants、compoundVariants,输出 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 声明。
