跳到主要内容

Token 化与 Headless 组件阶段

要点

  • API 与样式解耦的 Headless 组件(Radix、Headless UI、shadcn/ui、reka-ui)+ Tailwind/Uno,让设计 token 可直连类名。
  • cva/tailwind-variants 集中声明 variants/default/compound,配合 tailwind-merge 兜底;最利于 AI/脚本生成。
  • 适合多品牌/暗色/大规模设计体系;前提是 tokens、lint/merge/评审链到位。
  • 代表性包:Primitives(@radix-ui/react-*@ark-ui/react@zag-js)、Headless 组件库(@headlessui/react)、样式模板化方案(shadcn/uireka-uivaul),加上 class 构造器(class-variance-authoritytailwind-variantstailwind-merge)。

优势 / 劣势 / 何时使用

内容
优势API 与样式分离;变体集中;对 AI/自动化友好;跨品牌/主题低成本
劣势需要维护 token 表与规范;初学者心智负担高;文档/范式建设成本高
适用需要统一体验、多产品线、多主题/品牌、希望自动化生成 UI 的团队
不适用极简站点或无设计体系、无法投入规范建设的团队

什么是 token?什么是 token 化

  • Design Token:把颜色、间距、圆角、字号、阴影、动效等设计属性抽象成命名变量(如 color-primary-500radius-lg),集中管理,作为唯一取值来源。
  • Token 化:将设计稿中的具体数值(#123456、18px 等)转译为 token 名称,在代码/样式里只引用 token,不写裸值;同时保留映射表与校验链,确保所有取值来自 token。
  • 落地方式
    • CSS 变量::root { --color-primary-500: #2563eb; --radius-lg: 12px; },配合 Tailwind @theme 或自定义 theme 读取。
    • Tailwind 配置:在 theme.colors/spacing/radius 中声明,生成对应原子类。
    • 多主题/多品牌:通过 data-theme 或类名切换一组 token 变量,实现暗色/品牌切换而不改组件逻辑。
  • 收益:统一视觉取值,减少随手取值漂移;便于 lint/merge/脚本校验;支持跨品牌/主题的快速切换。
  • 注意:token 只是基石,还需 cva/tailwind-variants 把 token 编织成变体,再由 Headless 组件消费。

代表性包与组合方式

  • Radix Primitives / Ark UI:提供无样式、可访问性完善的组件原件。
Radix + Tailwind
import * as Tabs from '@radix-ui/react-tabs'

export function TabsDemo() {
return (
<Tabs.Root defaultValue="code" className="w-full">
<Tabs.List className="inline-flex gap-2 rounded-lg bg-muted p-1">
<Tabs.Trigger value="code" className="rounded-md px-3 py-2 text-sm data-[state=active]:bg-card data-[state=active]:shadow">
代码
</Tabs.Trigger>
<Tabs.Trigger value="preview" className="rounded-md px-3 py-2 text-sm data-[state=active]:bg-card data-[state=active]:shadow">
预览
</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
)
}
  • Headless UI:Vue/React 可组合的无样式组件,常与 Tailwind 搭配。
Headless UI + Tailwind
import { Menu } from '@headlessui/react'

export function MenuDemo() {
return (
<Menu as="div" className="relative inline-block text-left">
<Menu.Button className="inline-flex items-center gap-2 rounded-lg border px-3 py-2">操作</Menu.Button>
<Menu.Items className="absolute right-0 mt-2 w-40 rounded-xl border bg-card p-2 shadow-xl">
<Menu.Item>
{({ active }) => <button className={`w-full rounded-lg px-3 py-2 text-sm ${active ? 'bg-muted' : ''}`}>编辑</button>}
</Menu.Item>
</Menu.Items>
</Menu>
)
}
  • shadcn/ui / reka-ui 模板化:基于 Tailwind + cva/tailwind-variants 预置 class,直接复制到项目。
tailwind-variants 组合
import { tv } from 'tailwind-variants'
import { Slot } from '@radix-ui/react-slot'
import { cn } from '@/lib/utils'

const badge = tv({
base: 'inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-medium',
variants: {
tone: {
neutral: 'bg-muted text-foreground',
success: 'bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-100',
danger: 'bg-rose-50 text-rose-700 dark:bg-rose-900/30 dark:text-rose-100',
},
},
defaultVariants: { tone: 'neutral' },
})

export function Badge({ asChild, className, ...props }: { asChild?: boolean } & React.HTMLAttributes<HTMLElement>) {
const Comp = asChild ? Slot : 'span'
return <Comp className={cn(badge(props), className)} {...props} />
}

tokens → variants → primitives 流程(示意)

Tokens:统一色板/间距/圆角/排版,作为唯一取值来源。
Factory:用 cva/tv 把尺寸、语义、状态集中声明。
Primitives/UI:Headless 组件消费变体,组合成业务 UI。
Lint/Merge:校验取值合法,合并类名,防止越界与冲突。

示例(Button with cva,React)

import { cva } from 'class-variance-authority'
import { cn } from '@/lib/utils'

const button = cva(
'inline-flex items-center gap-2 rounded-lg border bg-primary px-4 py-2 text-sm text-primary-foreground shadow-sm transition',
{
variants: {
variant: { default: '', outline: 'bg-transparent text-foreground border-border', ghost: 'bg-transparent hover:bg-muted' },
size: { sm: 'h-8 px-3', md: 'h-10 px-4', lg: 'h-11 px-5' },
},
compoundVariants: [{ variant: 'outline', size: 'lg', class: 'shadow-none' }],
defaultVariants: { variant: 'default', size: 'md' },
}
)

export function Button({ className, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
return <button className={cn(button(props), className)} {...props} />
}

对齐建议

  • tokens:颜色/间距/圆角/阴影/字号写成 tokens(CSS 变量或 @theme),保持「设计稿 → token 名」映射。
  • variants:所有状态/尺寸/语义集中到工厂(cva/tv),业务组件只消费 builder;默认值写死。
  • merge:统一使用 cn(含 tailwind-merge)兜底,避免 class 冲突。
  • 资产:记录推荐组合(例如按钮/表单/卡片的标准类名),便于 AI/新人沿用。