mcpField
S
shadcn-ui

shadcn-ui

github.com/oimiragieo设计与 UI4438 次安装17 周前更新
代码仓库

关于 shadcn-ui

面向 React 和 Next.js 开发者的技能,指导使用 shadcn/ui 组件、样式、主题及无障碍模式。组件由 CLI 复制到项目中,便于直接拥有和定制,而非作为软件包安装。

主要功能

  1. 01通过 CLI 将组件复制到项目
  2. 02使用 Tailwind CSS 变量主题
  3. 03指导设置深色模式
  4. 04涵盖表单、表格和对话框
  5. 05介绍基于 Radix 的无障碍模式

适用场景

  1. 01向 React 项目添加 shadcn/ui 组件
  2. 02设置深色模式和主题令牌
  3. 03构建无障碍表单和对话框
  4. 04选择组件或自定义 UI

安装

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

$npx skills add https://github.com/oimiragieo/agent-studio --skill shadcn-ui

安装后在 Claude Code 中输入/shadcn-ui确认已加载。

包含的文件

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

commands/
shadcn-ui.md
hooks/
post-execute.cjs
pre-execute.cjs
references/
research-requirements.md
rules/
shadcn-ui.md
schemas/
input.schema.json
output.schema.json
scripts/
main.cjs
templates/
implementation-template.md

详细介绍

shadcn/ui 专家指南

全面介绍如何使用 shadcn/ui 构建 UI——这是一个基于 Radix UI 原语和 Tailwind CSS 构建的复制粘贴式组件库。组件不会作为依赖项安装,而是复制到你的项目中,供你完全掌控和自定义。

适用场景

在以下情况下使用此技能:

  • 向 React 或 Next.js 项目添加 shadcn/ui 组件
  • 自定义组件样式、变体或行为
  • 使用 CSS 变量设置 Tailwind CSS v4 主题
  • 使用 shadcn/ui 实现深色模式
  • 构建无障碍表单、对话框或数据表格
  • 在 shadcn/ui 组件和自定义实现之间进行选择

核心概念

shadcn/ui 不是组件库

shadcn/ui 是一组可复用组件,你可以将其复制到自己的项目中。它与传统组件库的主要区别:

  • 无需 npm 包依赖——组件位于你的代码库中
  • 完全掌控——可自由修改任何组件
  • Radix UI 原语——底层是无障碍、无样式的无头组件
  • Tailwind CSS——所有样式均通过实用类和 CSS 变量实现
  • 由 CLI 驱动——npx shadcn@latest add button 会复制组件代码
架构
Your Project
  components/
    ui/              <- shadcn/ui components live here
      button.tsx
      dialog.tsx
      input.tsx
      ...
  lib/
    utils.ts         <- cn() utility (clsx + tailwind-merge)

设置

Next.js App Router(推荐)
# Initialize shadcn/ui in existing Next.js project
npx shadcn@latest init

# This creates:
# - components.json (configuration)
# - lib/utils.ts (cn utility)
# - Tailwind CSS variable theme in globals.css

components.json 配置:

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/globals.css",
    "baseColor": "zinc",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  }
}
Vite + React
# Initialize
npx shadcn@latest init

# Vite requires path aliases in vite.config.ts:
import path from 'path';

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});
Next.js Pages Router

与 App Router 相同,但需在 components.json 中设置 rsc: false,因为 Pages Router 不支持 React Server Components。

添加组件

# Add a single component
npx shadcn@latest add button

# Add multiple components
npx shadcn@latest add button card input label

# Add all components
npx shadcn@latest add --all

# View available components
npx shadcn@latest add --list
cn() 实用函数

每个 shadcn/ui 组件都使用 cn() 来合并条件类名:

// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

cn() 将 clsx(条件类名)与 tailwind-merge(解决 Tailwind 冲突)结合起来:

<Button
  className={cn(
    'bg-primary text-white',
    isDisabled && 'opacity-50 cursor-not-allowed',
    size === 'lg' && 'px-8 py-4 text-lg'
  )}
>
  Submit
</Button>

使用 CSS 变量设置主题

Tailwind CSS v4 主题设置

shadcn/ui 使用 CSS 自定义属性设置主题,从而支持运行时切换主题:

/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 240 10% 3.9%;
    --card: 0 0% 100%;
    --card-foreground: 240 10% 3.9%;
    --popover: 0 0% 100%;
    --popover-foreground: 240 10% 3.9%;
    --primary: 240 5.9% 10%;
    --primary-foreground: 0 0% 98%;
    --secondary: 240 4.8% 95.9%;
    --secondary-foreground: 240 5.9% 10%;
    --muted: 240 4.8% 95.9%;
    --muted-foreground: 240 3.8% 46.1%;
    --accent: 240 4.8% 95.9%;
    --accent-foreground: 240 5.9% 10%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 0 0% 98%;
    --border: 240 5.9% 90%;
    --input: 240 5.9% 90%;
    --ring: 240 5.9% 10%;
    --radius: 0.5rem;
  }

  .dark {
    --background: 240 10% 3.9%;
    --foreground: 0 0% 98%;
    --card: 240 10% 3.9%;
    --card-foreground: 0 0% 98%;
    --popover: 240 10% 3.9%;
    --popover-foreground: 0 0% 98%;
    --primary: 0 0% 98%;
    --primary-foreground: 240 5.9% 10%;
    --secondary: 240 3.7% 15.9%;
    --secondary-foreground: 0 0% 98%;
    --muted: 240 3.7% 15.9%;
    --muted-foreground: 240 5% 64.9%;
    --accent: 240 3.7% 15.9%;
    --accent-foreground: 0 0% 98%;
    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 0 0% 98%;
    --border: 240 3.7% 15.9%;
    --input: 240 3.7% 15.9%;
    --ring: 240 4.9% 83.9%;
  }
}
深色模式实现

在 Next.js 中使用 next-themes 实现深色模式:

// app/providers.tsx
'use client';

import { ThemeProvider } from 'next-themes';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
      {children}
    </ThemeProvider>
  );
}

// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

切换组件:

'use client';

import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
import { Moon, Sun } from 'lucide-react';

export function ThemeToggle() {
  const { setTheme, theme } = useTheme();

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
    >
      <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
      <span className="sr-only">Toggle theme</span>
    </Button>
  );
}

常见组件模式

使用 React Hook Form + Zod 构建表单
'use client';

import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';

const formSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});

export function LoginForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: { email: '', password: '' },
  });

  function onSubmit(values: z.infer<typeof formSchema>) {
    console.log(values);
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input placeholder="[email protected]" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="password"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Password</FormLabel>
              <FormControl>
                <Input type="password" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Sign In</Button>
      </form>
    </Form>
  );
}
使用 TanStack Table 构建数据表格
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';

interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[];
  data: TData[];
}

export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <div className="rounded-md border">
      <Table>
        <TableHeader>
          {table.getHeaderGroups().map(headerGroup => (
            <TableRow key={headerGroup.id}>
              {headerGroup.headers.map(header => (
                <TableHead key={header.id}>
                  {flexRender(header.column.columnDef.header, header.getContext())}
                </TableHead>
              ))}
            </TableRow>
          ))}
        </TableHeader>
        <TableBody>
          {table.getRowModel().rows.map(row => (
            <TableRow key={row.id}>
              {row.getVisibleCells().map(cell => (
                <TableCell key={cell.id}>
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </TableCell>
              ))}
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  );
}
响应式对话框/抽屉模式

在桌面端使用 Dialog,在移动端使用 Drawer:

'use client';

import { useMediaQuery } from '@/hooks/use-media-query';
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer';
import { Button } from '@/components/ui/button';

export function ResponsiveModal({ children }: { children: React.ReactNode }) {
  const isDesktop = useMediaQuery('(min-width: 768px)');

  if (isDesktop) {
    return (
      <Dialog>
        <DialogTrigger asChild>
          <Button>Open</Button>
        </DialogTrigger>
        <DialogContent>{children}</DialogContent>
      </Dialog>
    );
  }

  return (
    <Drawer>
      <DrawerTrigger asChild>
        <Button>Open</Button>
      </DrawerTrigger>
      <DrawerContent>{children}</DrawerContent>
    </Drawer>
  );
}

无障碍模式

shadcn/ui 组件基于 Radix UI 构建,后者提供:

  • 完整的键盘导航(Tab、方向键、Enter、Escape)
  • ARIA 属性(角色、状态、属性)
  • 焦点管理(焦点陷阱、焦点恢复)
  • 屏幕阅读器播报
各组件的关键无障碍特性
组件键盘ARIA焦点陷阱
Button按 Enter/Space 激活role="button"否
Dialog按 Escape 关闭role="dialog", aria-modal是
Dropdown Menu使用方向键导航role="menu", role="menuitem"是
Select方向键、按键搜索role="listbox", role="option"是
Tabs使用方向键切换标签页role="tablist", role="tab"否
Toast自动播报role="status", aria-live否
Tooltip聚焦/悬停时显示role="tooltip"否
自定义无障碍增强
// Always provide labels for interactive elements
<Button aria-label="Close dialog">
  <X className="h-4 w-4" />
</Button>

// Use sr-only for visual-only content
<span className="sr-only">Loading...</span>

// Announce dynamic content
<div aria-live="polite" aria-atomic="true">
  {statusMessage}
</div>

反模式

  • 不要将 shadcn/ui 安装为 npm 包——使用 CLI 复制组件
  • 不要直接修改 Radix 原语——通过 shadcn 包装组件扩展
  • 不要使用硬编码颜色——始终使用 CSS 变量主题令牌
  • 不要跳过 cn() 实用函数——它可以防止 Tailwind 类冲突
  • 使用 next-themes 时,不要忘记在 <html> 上添加 suppressHydrationWarning
  • 不要嵌套交互元素(例如在 button 中放置 button,或在 button 中放置链接)

铁律

  1. 绝不将 shadcn/ui 安装为软件包依赖项——组件必须复制到项目中,以便完全拥有
  2. 始终使用 cn() 工具处理条件类名,避免 Tailwind 类冲突
  3. 绝不硬编码颜色——始终使用 CSS 变量主题令牌,确保主题一致性
  4. 始终通过 shadcn/ui 抽象层使用 Radix UI 基元,不要直接使用
  5. 绝不嵌套交互元素(按钮中放按钮、按钮中放链接)——这违反无障碍标准

反模式

反模式失败原因正确做法
作为软件包安装组件源码被锁定,无法自定义使用 npx shadcn@latest add 将组件复制到项目中
硬编码颜色值主题切换会失效;深色模式无法正常工作使用 CSS 变量令牌(bg-background、text-foreground 等)
跳过 cn() 工具Tailwind 类冲突会导致样式不可预测始终使用 @/lib/utils 中的 cn() 合并类名
直接使用 Radix UI 基元缺少 shadcn 样式和无障碍功能配置使用封装 Radix 基元并包含正确类名的 shadcn 组件
缺少 suppressHydrationWarning使用 next-themes 深色模式时会出现水合不匹配错误使用 next-themes 时,将 suppressHydrationWarning 添加到 <html>

记忆协议(强制)

开始之前: 阅读 .claude/context/memory/learnings.md

完成之后:

  • 新模式 -> .claude/context/memory/learnings.md
  • 发现问题 -> .claude/context/memory/issues.md
  • 做出决策 -> .claude/context/memory/decisions.md

**假设会发生中断:**如果没有记录在记忆中,就等于没有发生。

参考资料

评论

支持 Markdown