关于 web-ui-shadcn-ui
面向使用 shadcn/ui 构建界面的开发者,说明如何通过 CLI 添加并维护组件,以及如何编辑组件源码。指南还涉及主题变量、样式组合、表单布局和复合组件。
主要功能
- 01通过 CLI 添加及检查组件
- 02编辑自有组件源码与变体
- 03使用 OKLCH 变量配置主题
- 04通过 cn() 合并样式类
- 05使用 Field 组织表单布局
适用场景
- 01将注册表组件加入项目
- 02为组件增加样式变体或行为
- 03配置浅色与深色主题
- 04组合对话框、标签页及命令菜单
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-ui-shadcn-ui安装后在 Claude Code 中输入/web-ui-shadcn-ui确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/command-palette.mdcomposition.mdcore.mddata-table.mddialogs.mdforms.mdtheming.md详细介绍
shadcn/ui 组件模式
快速指南: shadcn/ui 是一种分发方式,而非依赖项——
npx shadcn@latest add会将组件源码写入你的代码库,从此由你维护。由此带来三点影响,决定了其他一切:自定义意味着编辑文件,而不是从外部覆盖;升级时需要审核差异,而不是提升版本号;组件本身由基础组件库和实用 CSS 组合而成,源码会按名称导入它们。使用 OKLCH 中的 CSS 自定义属性设置主题,通过cn()合并类名,并使用Field布局字段。Field已取代与表单库耦合的Form/FormField模式。
详细资源:
- examples/core.md —
components.json、cn()、骨架屏加载 - examples/composition.md — 扩展组件、自适应对话框/抽屉
- examples/forms.md —
Field、FieldGroup、FieldSet、旧版Form模式 - examples/dialogs.md — AlertDialog、Sheet、toast
- examples/data-table.md — 带可排序表头和行操作的表格
- examples/command-palette.md — 带键盘快捷键的命令菜单
- examples/theming.md — 自定义 OKLCH 颜色、深色模式、主题感知组件
- reference.md — CLI 命令、CSS 变量表、反模式、平台变更
适用路径
- 组件基于哪种基础组件库。
init --base radix和--base base会为同一注册表条目生成不同的组件源码。无论哪种方式,shadcn 层级的 API——导出名称、cn()、变体 props、data-slot——都相同,因此本指南中的内容始终适用;不同之处在于,编辑源码时需要使用哪个基础组件的 props。 - 使用 Field 还是旧版 Form。 新工作使用
Field及其同类组件,它们仅用于布局。已使用Form/FormField/FormItem/FormControl/FormMessage的代码库仍可正常工作;这些组件绑定到一种表单库,不应再用于新增字段。两种模式均见 examples/forms.md。
<critical_requirements>
编写 shadcn/ui 代码前
使用 npx shadcn@latest add <name> 添加组件。 CLI 会解析注册表条目的依赖项,安装所需的基础包,将文件写入 components.json 记录的路径,并将导入重写为你配置的别名——从文档中粘贴源码时,这些操作都不会发生。
更改组件行为前,先阅读组件源码。 它是你仓库中的文件,而不是一个包的边界,因此对于“我能否更改它”,答案始终是“可以”,值得问的问题是还有哪些地方导入了它。
所有类都通过 cn() 处理,并将传入的 className 放在最后。 它通过保留最后一个 Tailwind 工具类来解决冲突,这样调用方的 px-8 就会替换组件的 px-4,而不是与之并存,陷入优先级平局。
每种新增背景色都要配上前景色。 如果有 --brand 却没有 --brand-foreground,该表面上的文本就会继承之前的颜色,通常会在一种主题下通过检查,却在另一种主题下对比度不足。请在 :root 和 .dark 中都添加这两个变量,并添加将它们转换为工具类的 @theme inline 映射。
优先使用变体,而不是一次性类。 如果不止一个调用方需要某种样式,就应该将其加入组件的变体映射,在其中命名并定义类型;在各个调用处重复一组类名,意味着每次都要重新做出相同的决定。
</critical_requirements>
自动检测: shadcn/ui、shadcn、components.json、npx shadcn、shadcn@latest add、cn()、data-slot、@theme inline、--base radix、--base base、Field、FieldLabel、FieldDescription、FieldError、FieldGroup、FieldSet、FieldLegend、CommandDialog、SheetContent、AlertDialogAction、--primary-foreground、--sidebar-*、--chart-*
适用范围:
- 将组件添加到项目并保持更新——CLI、
components.json、差异 - 编辑自有组件源码:添加变体、属性或行为
- 主题契约——背景色/前景色配对、OKLCH 值、
@theme inline、暗色模式 - 组合注册表提供的复合组件:Card、Dialog、Sheet、Tabs、Command、Field
- 为特定交互场景选择注册表中功能重叠的组件
其他内容另行处理:
- 工具类编写和 CSS 流水线——本指南确定组件读取哪些变量;如何配置和构建工具层则是另一个主题
- 底层基础库——其属性、
asChild语义和无障碍契约,取决于项目所选的基础库 - 变体映射编写通法——本指南介绍如何扩展组件源码中已有的变体映射
- 表单状态、验证和提交——
Field负责布局和无障碍连接,不保存值
shadcn/ui 颠覆了通常的交易方式。组件库提供 API 并保留源码;而这里提供源码,除此之外一无保留。注册表只是一个起点,文件一旦落入你的项目,它就不再是上游版本。
这就是 CLI 比看起来更重要的原因。它不只是一个方便的复制粘贴封装——只有它了解注册表条目的依赖关系、你的别名配置以及你选择的基础库;也正是它让 --diff 日后能够告诉你,你的副本与注册表版本相差多远。
组件由什么构成,在这里被视为组合中的既定事实。 CLI 写入的源码会导入一个基础库、应用工具类并声明变体映射——不提及这些内容的指南,就无法描述读者正在查看的文件。讲解这些组成部分是另一项工作,已在上方的其他内容另行处理中说明。
<decision_framework>
选择覆盖层
Confirmation the user must answer?
└─ AlertDialog — no dismiss path except Cancel or Action
Form or detailed content?
├─ Wide viewport → Dialog (centred)
└─ Narrow viewport → Drawer (rises from the bottom)
Both at once → branch on a media query; see examples/composition.md
Editing in context, page still visible?
└─ Sheet — slides in from an edge
A short action or a single selection?
└─ Popover for content, DropdownMenu for a list of actions
选择字段控件
Text → Input, or Textarea when it wraps
Two to five options, all worth showing → RadioGroup
Many options → Select, or Combobox when it needs filtering
Several at once → Checkbox per option
A setting that takes effect immediately → Switch
An agreement or a term to accept → Checkbox
A date → Calendar, or DatePicker with a trigger
Whatever the control, wrap it in Field — not the legacy FormField.
</decision_framework>
核心模式
模式 1:安装和检查组件
CLI 是与注册表交互的接口。--dry-run、--diff 和 --view 都能在不写入任何内容的情况下解答有关组件的问题,因此可以据此检查升级。
npx shadcn@latest init # writes components.json
npx shadcn@latest add button card dialog
npx shadcn@latest add button --diff # how far your copy has drifted
npx shadcn@latest info # the resolved project context
完整命令列表:reference.md
模式 2:主题约定
每种颜色都是一对——一个表面色及其上显示的文本颜色——在 :root 中声明,在 .dark 中覆盖,并通过 @theme inline 暴露为实用类。新增颜色需要包含这三项,否则它只会存在于两种主题之一,或者不会出现在任何实用类中。
:root {
--brand: oklch(0.627 0.265 303.9);
--brand-foreground: oklch(1 0 0);
}
@theme inline {
--color-brand: var(--brand);
--color-brand-foreground: var(--brand-foreground);
}
--brand-foreground 是用于 --brand 之上 的文本颜色,而不是品牌色文本——这种命名方式与大多数约定正好相反,弄错会导致文本不可见。
完整代码:examples/theming.md
模式 3:cn() 以及顺序为何重要
cn() 用于解决冲突,而不是拼接。来自同一组的两个实用类会合并为最后一个,因此传入的 className 放在最后,调用方的覆盖值才会生效。
cn("px-4", "px-8"); // → "px-8", not "px-4 px-8"
<div className={cn("rounded-lg border bg-card shadow-sm", className)} />;
直接拼接字符串会让两个类都保留在列表中,此时哪个类生效取决于它们在生成的样式表中的顺序,而非调用位置。
完整代码:examples/core.md
模式 4:通过变体映射扩展组件
每个随包提供的组件都通过变体映射声明样式——由基础类列表以及命名的 variant 和 size 轴组成,属性类型则根据该映射推导。你可以在自己的源代码中编辑该映射来添加变体;不需要增强 API,因为没有这个必要。
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
brand: "bg-brand text-brand-foreground hover:bg-brand/90", // added
},
}
<Button variant="brand">Subscribe</Button>;
添加行为属性也是同样的做法——请参阅 examples/composition.md 中的加载按钮。
模式 5:使用 Field 布局表单
Field 及其同系列组件包含标签、描述、错误插槽以及它们之间的 ARIA 关联,且自身不持有任何值。因此它们可以与任何表单库、服务端操作搭配使用,也可以完全独立使用。
<Field data-invalid={hasError}>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" aria-invalid={hasError} />
<FieldDescription>We will never share your email.</FieldDescription>
{hasError && <FieldError errors={errors} />}
</Field>
约定包括以下三点:Field 上的 data-invalid、控件上的 aria-invalid,以及仅在存在错误时才渲染 FieldError。FieldGroup、FieldSet 和 FieldLegend 用于对字段分组;orientation 用于切换标签位置。
完整代码:examples/forms.md
模式 6:组合复合组件
Card、Dialog、Sheet、Tabs 和 Command 都由一组部件组成。通过包装这些部件来构建特定组件;将部件替换为普通元素会失去样式钩子,而对于覆盖层,还会失去其行为。
function ProductCard({ title, price }: ProductCardProps) {
return (
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>${price}</CardDescription>
</CardHeader>
</Card>
);
}
触发器上的 asChild 会将其合并到你的子元素上,而不是嵌套在子元素内部,因此用 Button 包装链接时,不会渲染出按钮内嵌套锚点的情况。
完整代码:examples/dialogs.md、examples/command-palette.md
<red_flags>
容易出错的地方
运行时会出问题:
- 没有
components.json— 所有 CLI 命令都会失败,因为别名、样式和基础原语都记录在其中 - 添加背景色却没有对应的
-foreground配对色 — 该表面上的文本会继承颜色,结果通常在一种主题下正常,在另一种主题下对比度不足 - 将颜色添加到
:root,却未添加到.dark,或未映射到@theme inline— 该颜色只存在于一种主题中,或者没有对应的工具类 - 对已经是
oklch(...)的值再套用hsl()— 会得到无效颜色,因此声明会被丢弃,元素会回退到继承的颜色 Button中包含链接,而不是通过asChild将其应用到链接上 — 在按钮中嵌套锚点属于无效 HTML,键盘用户会将其体验为一个令人困惑的控件- 为
Select设置了onValueChange,却既未提供value,也未提供defaultValue— 它永远不会显示选中项
容易令人意外的行为:
--primary-foreground是--primary上的文本颜色,不是主色文本- 拼接类名字符串而不是使用
cn(),会让相互冲突的工具类同时存在,因此最终生效的是哪一个取决于样式表顺序,而不是调用位置 - 编辑
components/ui/中的组件是预期工作流;之后可通过--diff查看你对照注册表所做的更改 - 在 React 19 下,随附组件将
ref作为普通属性接收,并通过data-slot标记其组成部分,而不是导出可供定位的类名 - 读取系统偏好的主题提供程序需要在
<html>上设置suppressHydrationWarning,因为服务器无法知道客户端会应用哪个类 Form/FormField/FormItem/FormControl/FormMessage仍然可用,也仍绑定到一个表单库 — 新字段应使用Field- 图表颜色会直接读取为
var(--chart-1);旧配置使用的hsl()包装现在是错误的,而不只是多余