Use Cases (更新: 2026/6/2)

Claude Code 搭配 shadcn/ui:React UI 实战指南

用 Claude Code 安全导入 shadcn/ui,完成 Button/Card/Form/Dialog/Table、设计令牌和 Playwright 视觉检查。

Claude Code 搭配 shadcn/ui:React UI 实战指南

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() 做视觉比较。

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.tsContactForm.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 数据,再把 customersonEdit 传给 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-invalidFieldError 后,差异审查明显轻松。

如果团队想稳定复用,可以把上面的提示词放进 CLAUDE.md。ClaudeCodeLab 也把这类 UI 规则、审查清单和 Claude Code 提示词整理成付费模板,适合希望减少每个 sprint 重复沟通成本的团队。

#Claude Code #shadcn/ui #React #Tailwind CSS #UI 库
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。