mcpField
W
web-styling-cva

web-styling-cva

github.com/agents-inc设计与 UI26140 次安装2 天前更新
代码仓库

关于 web-styling-cva

这是一份使用 Class Variance Authority(CVA)构建类型化视觉变体的技能指南,面向 UI 开发者。它将样式定义与 TypeScript 属性类型关联起来,并涵盖组合状态和多部分组件。

主要功能

  1. 01定义基础类名与变体组
  2. 02用 compoundVariants 设置组合样式
  3. 03通过 VariantProps 推导属性类型
  4. 04为可省略变体设置默认值
  5. 05使用 cx() 组合样式

适用场景

  1. 01建模尺寸、意图和状态变体
  2. 02为多个变体的组合设置样式
  3. 03保持属性类型与样式定义一致
  4. 04协调组件各部分的变体

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/agents-inc/skills --skill web-styling-cva

安装后在 Claude Code 中输入/web-styling-cva确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
composition.md
compound-variants.md
core.md

详细介绍

CVA(Class Variance Authority)模式

快速指南:cva() 会将组件的视觉状态转化为一个类型化配置对象:基础类、变体组(尺寸、意图、状态)、用于设置特定组合样式的 compoundVariants,以及在调用方省略选项时使用的 defaultVariants。VariantProps<typeof variants> 可从该对象推导出属性类型。它会生成类名字符串,但不会决定这些类名的含义,因此可与工具类、CSS 模块或普通 CSS 搭配使用。

详细资源:


<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">>;

完整代码:examples/composition.md


模式 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);
}

完整代码:examples/composition.md


模式 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 },
  }),
};

完整代码:examples/composition.md


模式 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 }));
}

完整代码:examples/composition.md


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

评论

支持 Markdown