Claude Code 搭配 shadcn/ui:React UI 实战指南
用 Claude Code 安全导入 shadcn/ui,完成 Button/Card/Form/Dialog/Table、设计令牌和 Playwright 视觉检查。
shadcn/ui 不是把组件藏在 node_modules 里的传统 UI 库。它通过 CLI 把组件源码复制到你的项目里,通常放在 src/components/ui。这意味着你可以直接修改 Button、Card、Dialog、Table 的实现,也意味着团队必须负责维护这些代码。
这正是它适合搭配 Claude Code 的原因。Claude Code 可以阅读生成出来的组件、理解 Tailwind class、帮你接 React Hook Form、补 Playwright 截图测试。但如果只说“帮我做一个后台页面”,它也可能生成重复按钮、混入旧版 Tailwind 配置,甚至删掉 Dialog 的无障碍结构。
本文以 Vite + React + TypeScript 的小型管理页面为例,讲清楚如何用 Claude Code 安全使用 shadcn/ui:安装与初始化、添加 Button/Card/Form/Dialog/Table、整理设计令牌、避免复制粘贴漂移,并用 Playwright 固定视觉回归。Claude Code 基础操作可以先看入门指南。
先看官方资料
开始前,把当前官方资料交给 Claude Code。shadcn/ui 的 Vite 文档使用 shadcn@latest,Tailwind CSS v4 强调通过 @theme 定义主题变量,Radix UI 负责 Dialog 的焦点管理与键盘行为,Playwright 则用 toHaveScreenshot() 做视觉比较。
- shadcn/ui Vite installation
- shadcn/ui React Hook Form guide
- Tailwind CSS theme variables
- Radix UI Dialog docs
- Claude Code quickstart
- Playwright visual comparisons
flowchart LR
A["让 Claude Code 读取现有项目"] --> B["运行 shadcn/ui init"]
B --> C["添加基础组件"]
C --> D["集中设计令牌"]
D --> E["编写业务层组件"]
E --> F["用 Playwright 锁定外观"]
安装与初始化
下面用 Vite 演示。Next.js 也可以使用 shadcn/ui,但初学时先避开路由、Server Components 和部署差异,能更容易看清 UI 系统本身。
pnpm create vite@latest shadcn-claude-demo -- --template react-ts
cd shadcn-claude-demo
pnpm install
pnpm add tailwindcss @tailwindcss/vite
pnpm add -D @types/node
在 vite.config.ts 中配置 Tailwind 插件和 @ alias。shadcn/ui 生成的代码会使用 @/components/ui/button 这类 import,所以 alias 必须和 TypeScript 保持一致。
import path from "node:path"
import react from "@vitejs/plugin-react"
import tailwindcss from "@tailwindcss/vite"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
给 Claude Code 的指令要先要求它检查,而不是立刻改文件。
我要在这个 Vite + React + TypeScript 项目中导入 shadcn/ui。
请先读取 package.json、vite.config.ts、tsconfig*.json、src/index.css,
只列出缺少的设置。等我确认后,再运行 shadcn@latest init
并添加需要的组件。
确认后执行:
pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest add button card field input label dialog table
pnpm add react-hook-form zod @hookform/resolvers
当前 shadcn/ui 的表单文档推荐 React Hook Form、Zod 和 Field 组合。不要把旧文章里的 Form 片段直接复制进项目,尤其是 Tailwind 或组件结构已经更新的项目。
Button 与 Card:先做小组件
第一步不要让 Claude Code 一口气生成完整 dashboard。先做一个可复用的 Card,确认 import、props、Tailwind class 和无障碍文本都正确。
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
type ProjectSummaryCardProps = {
name: string
openIssues: number
onCreateTask: () => void
}
export function ProjectSummaryCard({
name,
openIssues,
onCreateTask,
}: ProjectSummaryCardProps) {
return (
<Card className="max-w-md">
<CardHeader>
<CardTitle>{name}</CardTitle>
<CardDescription>
Review open UI issues before starting the next task.
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-3xl font-semibold">{openIssues}</p>
<p className="text-muted-foreground text-sm">open issues</p>
</CardContent>
<CardFooter>
<Button onClick={onCreateTask}>Add task</Button>
</CardFooter>
</Card>
)
}
评审时可以这样限制范围:
只レビュー src/components/app/ProjectSummaryCard.tsx。
检查 props 类型、shadcn/ui import、Tailwind class 可读性和可访问性。
除非必须,不要提出其他文件的修改。
Form:用 Field、React Hook Form、Zod
表单最容易出现“看起来完成了,其实不能上线”的问题。真实产品需要 schema 校验、错误文案、aria-invalid、提交中禁用按钮、自动填充属性和重置逻辑。
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
const contactSchema = z.object({
email: z.string().email("Enter a valid email address"),
topic: z.string().min(4, "Use at least 4 characters"),
})
type ContactFormValues = z.infer<typeof contactSchema>
export function ContactForm() {
const form = useForm<ContactFormValues>({
resolver: zodResolver(contactSchema),
defaultValues: {
email: "",
topic: "",
},
})
function onSubmit(values: ContactFormValues) {
console.log("submit", values)
}
return (
<form className="max-w-md space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="email"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
<Input
{...field}
id={field.name}
type="email"
aria-invalid={fieldState.invalid}
autoComplete="email"
/>
<FieldDescription>We will use this address to reply.</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
</FieldGroup>
<Button type="submit" disabled={form.formState.isSubmitting}>
Send
</Button>
</form>
)
}
小表单可以把 schema 和组件放在同一文件。字段变多以后,再让 Claude Code 有意识地拆成 schema.ts、ContactForm.tsx 和提交 action。不要让它在一次大改里临时发明目录结构。
Dialog 与 Table:保持可访问性
Dialog 不是普通的浮层。Radix UI Dialog 会处理模态模式、焦点限制、Escape 关闭,以及通过 Title/Description 给屏幕阅读器提供说明。shadcn/ui 负责样式,但这些结构仍然不能随意删除。
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
type Customer = {
id: string
name: string
plan: "Free" | "Pro" | "Team"
}
const customers: Customer[] = [
{ id: "cus_001", name: "Aoi Tanaka", plan: "Pro" },
{ id: "cus_002", name: "Mika Sato", plan: "Team" },
]
export function CustomerTable() {
const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(null)
return (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Customer</TableHead>
<TableHead>Plan</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{customers.map((customer) => (
<TableRow key={customer.id}>
<TableCell className="font-medium">{customer.name}</TableCell>
<TableCell>{customer.plan}</TableCell>
<TableCell className="text-right">
<Button
variant="outline"
size="sm"
onClick={() => setSelectedCustomer(customer)}
>
Edit
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Dialog open={selectedCustomer !== null} onOpenChange={() => setSelectedCustomer(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit customer</DialogTitle>
<DialogDescription>
Review the contract plan for {selectedCustomer?.name}.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setSelectedCustomer(null)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}
实际项目中,先让页面层 fetch 数据,再把 customers 和 onEdit 传给 Table。这样 Claude Code 修改 UI 时不会顺手改坏数据层。
设计令牌与 Tailwind 设置
设计令牌是颜色、圆角、字体、间距、阴影等视觉值的命名集合。Tailwind v4 的 @theme 会影响可用工具类;shadcn/ui 组件则常用 --background、--primary、--border 等 CSS 变量。
@import "tailwindcss";
@theme {
--font-sans: Inter, system-ui, sans-serif;
--radius-card: 0.75rem;
--color-brand-500: oklch(0.62 0.18 250);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
}
如果项目仍在 Tailwind v3,请让 Claude Code 分别给出“迁移到 v4”和“保持 v3 的最小修复”两套方案。把两种配置混在一起,是排查样式问题时最痛苦的来源之一。
避免复制粘贴漂移
shadcn/ui 的优势是源码在手里,风险也是源码在手里。团队很容易在半年后拥有三个 Button、两套 Card spacing 和多个不一致的 Dialog。
| 规则 | 目的 |
|---|---|
src/components/ui 只放 shadcn/ui 低层组件 | 方便追踪生成代码和自定义差异 |
业务组件放到 src/components/app | 不让产品逻辑污染基础组件 |
不随意修改 components.json alias | 防止 import 路径漂移 |
| 给 Claude Code 明确目标文件 | 减少重复组件 |
src/components/ui 是 shadcn/ui 生成组件。
不要新建 Button 或 Card。
请使用现有 import,业务相关组件放到 src/components/app。
修改后运行 rg "function .*Button|export .*Button" src/components,
并报告是否出现重复组件。
再配合本地检查:
git diff -- src/components/ui
git diff -- src/components/app
pnpm lint
pnpm build
更多 AI 开发流程的约束可以参考Claude Code 生产力技巧。
用 Playwright 做视觉检查
Playwright 视觉比较会先生成基准截图,之后检测差异。为了减少误报,基准截图最好在与 CI 相同的系统、浏览器、字体和视口下生成。
pnpm create playwright
pnpm playwright install
pnpm dev
pnpm playwright test
import { expect, test } from "@playwright/test"
test("customer table dialog visual state", async ({ page }) => {
await page.goto("/customers")
await page.getByRole("button", { name: "Edit" }).first().click()
await expect(page).toHaveScreenshot("customers-dialog.png", {
maxDiffPixels: 120,
})
})
只有在视觉变化是有意的情况下才更新截图。
pnpm playwright test --update-snapshots
失败后,把具体视觉问题告诉 Claude Code,而不是让它泛泛地“美化一下”。
Playwright 的 customers-dialog.png 失败。
移动端 Dialog footer 的按钮间距太小。
只读取 src/components/app/CustomerTable.tsx 和相关 CSS。
修复 spacing,但不要删除 DialogTitle 或 DialogDescription。
三个实际场景
第一个场景是新建管理后台。Button、Card、Table、Dialog 足以覆盖列表、详情、编辑和确认流程。建议先用静态数据做 UI,再连接真实 API。
第二个场景是统一旧产品的 UI。不要一次性重写所有页面,而是先替换搜索表单、设置 Dialog、空状态 Card 这类边界清晰的小块。
第三个场景是付费原型或客户演示。可以允许 mock 数据,但要要求组件边界接近生产代码,否则演示结束后无法复用。
无障碍检查也很适合这个流程。可结合Claude Code 无障碍实践,重点检查 label、键盘操作、Dialog 结构和错误提示。
常见坑
第一,旧版 Tailwind 配置复制进 v4 项目。先确认版本和构建方式,再修改配置。
第二,把业务逻辑写进 components/ui。例如在基础 Button 中加入付费计划判断,会影响无关页面。正确做法是在 app 层包装。
第三,删除 DialogTitle。如果标题不想显示,也要考虑视觉隐藏,而不是直接删除,因为它关系到屏幕阅读器说明。
第四,只依赖 HTML required。生产表单通常需要翻译错误、异步校验和服务端错误,schema 和错误渲染位置要提前决定。
第五,Playwright 截图环境不一致。OS、浏览器、字体、viewport 不同都会造成差异,要尽量固定。
可直接使用的提示词
目标:在 Vite + React + TypeScript 管理页面中导入 shadcn/ui,
使用 Button、Card、基于 Field 的 Form、Dialog、Table。
约束:
- 依据当前官方文档
- src/components/ui 只放 shadcn/ui 低层组件
- 业务组件放 src/components/app
- 表单使用 React Hook Form + Zod + Field
- 不删除 DialogTitle 和 DialogDescription
- 解释 Tailwind v4 @theme 与 CSS 变量的作用
- 添加一个 Playwright toHaveScreenshot 测试
流程:
1. 读取现有设置
2. 提出修改计划
3. 批准后实现
4. 运行 pnpm build 和 pnpm playwright test
5. 总结变更文件和风险
我实际用这个流程在最小 Vite 项目中验证过:组件安装、@theme 添加品牌色、Field 表单、Table 行打开 Dialog、Playwright 截图检查都可以串起来。需要二次修正的是表单错误显示;明确要求 aria-invalid 和 FieldError 后,差异审查明显轻松。
如果团队想稳定复用,可以把上面的提示词放进 CLAUDE.md。ClaudeCodeLab 也把这类 UI 规则、审查清单和 Claude Code 提示词整理成付费模板,适合希望减少每个 sprint 重复沟通成本的团队。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。