关于 web-styling-cva
这是一份使用 Class Variance Authority(CVA)构建类型化视觉变体的技能指南,面向 UI 开发者。它将样式定义与 TypeScript 属性类型关联起来,并涵盖组合状态和多部分组件。
主要功能
- 01定义基础类名与变体组
- 02用 compoundVariants 设置组合样式
- 03通过 VariantProps 推导属性类型
- 04为可省略变体设置默认值
- 05使用 cx() 组合样式
适用场景
- 01建模尺寸、意图和状态变体
- 02为多个变体的组合设置样式
- 03保持属性类型与样式定义一致
- 04协调组件各部分的变体
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-styling-cva安装后在 Claude Code 中输入/web-styling-cva确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/composition.mdcompound-variants.mdcore.md详细介绍
CVA(Class Variance Authority)模式
快速指南:
cva()会将组件的视觉状态转化为一个类型化配置对象:基础类、变体组(尺寸、意图、状态)、用于设置特定组合样式的compoundVariants,以及在调用方省略选项时使用的defaultVariants。VariantProps<typeof variants>可从该对象推导出属性类型。它会生成类名字符串,但不会决定这些类名的含义,因此可与工具类、CSS 模块或普通 CSS 搭配使用。
详细资源:
- examples/core.md — 基础变体、布尔状态、多个变体组、必填变体
- examples/compound-variants.md — 多条件样式、数组语法、状态矩阵
- examples/composition.md —
VariantProps、来自作用域样式表的类名、调用处追加与合并、多部分组件、扩展变体 - reference.md — API 签名、模式选择表、发布前检查清单
<critical_requirements>
编写 CVA 代码前
**在 variants 对象中定义每个变体选项。**在 cva() 外部编写条件类逻辑会将定义拆成两部分,推导出的类型也就无法继续描述组件。
**使用 VariantProps<typeof variants> 提取属性类型。**手写的变体类型会在添加或重命名变体时逐渐失准;从对象推导的类型则不会。
**为调用方可能省略的每个变体设置 defaultVariants。**否则,不带参数调用时只会返回基础类——没有意图,也没有尺寸。
**当两个变体组合后需要第三种样式时,使用 compoundVariants。**它可以在定义中一次性声明“当 X 和 Y 时,同时应用 Z”,无需在每个调用处重复编写。
</critical_requirements>
自动检测: cva、class-variance-authority、VariantProps、compoundVariants、defaultVariants、cx
适用场景:
- 具有多个视觉维度的组件——尺寸、意图、色调、状态
- 需要专属样式的组合(“大号主按钮使用大写字母”)
- 从样式定义推导组件属性类型
- 需要让各部分保持视觉协调的多部分组件
其他内容:
- 类名的含义——
cva()会生成字符串,类名背后的 CSS 由其所属方决定 - 两个冲突类名之间的冲突解决——
cx()会拼接并过滤假值;px-2和px-8哪个优先,应由合并工具决定 - 随视口变化的样式——每次渲染时只会选择一个变体,因此断点应写在样式表中
- 运行时计算的值(测量出的宽度、来自数据的颜色)——变体是固定选项集;运行时值应写入内联样式或自定义属性
CVA 将变体视为 UI 状态的类型系统。变体表是唯一事实来源:类字符串和属性类型都由它生成,因此添加选项时只需修改一个对象,TypeScript 就会自动处理其余部分。定义描述的是每种状态 呈现出来的样子,而不是如何计算类列表——这就是为什么同一定义可以原样适用于不同的 UI 框架和 CSS 方案。
核心模式
模式 1:基础变体定义
基础类加上每个变体组各自的条目。当一个组的类超过两个时,使用数组比空格分隔的字符串更易读。
import { cva } from "class-variance-authority";
const buttonVariants = cva(["font-semibold", "border", "rounded"], {
variants: {
intent: {
primary: ["bg-blue-600", "text-white"],
secondary: ["bg-white", "text-gray-800"],
},
size: {
sm: ["text-sm", "py-1", "px-2"],
md: ["text-base", "py-2", "px-4"],
},
},
defaultVariants: { intent: "primary", size: "md" },
});
buttonVariants(); // primary + md
buttonVariants({ intent: "secondary" }); // secondary + md
完整代码:examples/core.md
模式 2:布尔变体
true/false 键用于建模二元状态。应同时定义两种情况——false 分支用于设置常规状态的样式。
const inputVariants = cva(["border", "rounded", "px-3", "py-2"], {
variants: {
disabled: {
false: ["bg-white", "cursor-text"],
true: ["bg-gray-100", "cursor-not-allowed"],
},
error: { false: ["border-gray-300"], true: ["border-red-500"] },
},
defaultVariants: { disabled: false, error: false },
});
完整代码:examples/core.md
模式 3:复合变体
当 compoundVariants 条目中的每个键都匹配时,该条目就会生效。数组值会匹配其中任意一项,从而合并几乎相同的规则。
const buttonVariants = cva(["font-semibold", "rounded"], {
variants: {
intent: { primary: ["bg-blue-600"], secondary: ["bg-white"] },
disabled: { false: null, true: ["opacity-50", "cursor-not-allowed"] },
},
compoundVariants: [
// Hover only where the button is interactive
{ intent: "primary", disabled: false, class: ["hover:bg-blue-700"] },
{ intent: "secondary", disabled: false, class: ["hover:bg-gray-100"] },
{
intent: ["primary", "secondary"],
disabled: true,
class: ["pointer-events-none"],
},
],
defaultVariants: { intent: "primary", disabled: false },
});
完整代码:examples/compound-variants.md
模式 4:使用 VariantProps 提取类型
VariantProps 会读取定义,并为每个变体组生成一个可选且可为 null 的属性。
import { cva, type VariantProps } from "class-variance-authority";
const cardVariants = cva(["rounded-lg", "border"], {
variants: {
elevation: { flat: ["shadow-none"], raised: ["shadow-md"] },
padding: { none: ["p-0"], sm: ["p-2"], md: ["p-4"] },
},
defaultVariants: { elevation: "flat", padding: "md" },
});
type CardVariants = VariantProps<typeof cardVariants>;
// { elevation?: "flat" | "raised" | null; padding?: "none" | "sm" | "md" | null }
// Make one variant mandatory: leave it out of defaultVariants, then narrow the type
type CardProps = Omit<CardVariants, "padding"> &
Required<Pick<CardVariants, "padding">>;
模式 5:使用 cx() 合并类名
cx() 随包一同提供(重新导出自 clsx):它会连接值并丢弃假值。冲突解决则由单独的工具负责。
import { cva, cx } from "class-variance-authority";
cx(buttonVariants({ intent: "primary" }), highlighted && "ring-2", className);
// Where a caller's className must be able to override a variant's own padding,
// wrap the call in a merging utility instead of concatenating
function button(variants: ButtonVariants, className?: string): string {
return cn(buttonVariants(variants), className);
}
模式 6:多部分组件
每个带样式的部分使用一个 cva(),并共享变体名称,让各部分保持同步。
const formFieldVariants = {
label: cva(["block", "font-medium"], {
variants: { size: { sm: ["text-sm"], md: ["text-base"] } },
defaultVariants: { size: "md" },
}),
input: cva(["w-full", "border", "rounded"], {
variants: {
size: { sm: ["text-sm", "px-2"], md: ["text-base", "px-3"] },
error: { false: ["border-gray-300"], true: ["border-red-500"] },
},
defaultVariants: { size: "md", error: false },
}),
helper: cva(["mt-1"], {
variants: {
size: { sm: ["text-xs"], md: ["text-sm"] },
error: { false: ["text-gray-500"], true: ["text-red-600"] },
},
defaultVariants: { size: "md", error: false },
}),
};
模式 7:组合和扩展定义
使用 cx() 组合两个定义——一个共享的可交互基础定义,以及组件自己的变体。它们的 VariantProps 会交叉组合为一个属性类型。
const interactiveVariants = cva(["transition-colors", "focus:ring-2"], {
variants: { focusRing: { blue: ["focus:ring-blue-500"] } },
defaultVariants: { focusRing: "blue" },
});
type ButtonProps = VariantProps<typeof interactiveVariants> &
VariantProps<typeof buttonVariants>;
function button({ focusRing, intent }: ButtonProps): string {
return cx(interactiveVariants({ focusRing }), buttonVariants({ intent }));
}
<red_flags>
注意事项
生成了错误的类名:
- 没有
defaultVariants—buttonVariants()只会返回基础类名,因此未传入属性时渲染的组件会失去其意图和尺寸。为每个可选变体设置默认值。 - 布尔变体只有
true键 — 正常状态不会获得任何类名。为false分支编写基础样式,或者在确实不需要任何样式时使用null。 - 手写变体属性类型 — 它们可能接受定义中已移除的值,也可能拒绝后来添加的值。
VariantProps<typeof variants>不会与定义脱节。 - 在定义之外使用条件类名逻辑 — 变体表不再是唯一事实来源,派生类型也只能描述组件的一部分。将条件添加为变体。
- 使用嵌套三元表达式处理组合状态 — 无法访问的分支不易察觉;
compoundVariants则会枚举这些组合。
容易引起意外的行为:
VariantProps会让每个变体都变为可选且可为 null。使用Required<Pick<T, "color">>可将其中一个设为必填。compoundVariants类名会出现在返回字符串中的常规变体类名之后。字符串顺序不代表 CSS 优先级,因此两个相冲突的类名仍需要使用合并工具。- 基础类名始终生效,任何变体都无法移除它。只有部分状态需要的类名应放在变体中,而不是基础定义中。
null是有效的变体值,表示“此选项不添加任何内容”——这与省略键不同,省略键会让选项保持未定义状态。class和className都可以用于compoundVariants条目中。每个项目应选用其中一种;混用会让条目看起来像是不同的结构。
</red_flags>