DevToolBox免费
博客

shadcn/ui 完全指南:美观可访问的 React 组件

20 min read作者 DevToolBox Team

shadcn/ui 改变了开发者对 React 组件库的认知。它不是传统的 npm 包,而是让你将精心设计的无障碍组件直接复制到项目中。你拥有代码所有权,可以自定义一切,没有依赖锁定。基于 Radix UI 原语和 Tailwind CSS 构建,shadcn/ui 已成为 2026 年构建生产级 React 界面的首选。

TL;DR

shadcn/ui 是一组可复用的无障碍 React 组件,你将它们复制到项目中而非作为依赖安装。基于 Radix UI 和 Tailwind CSS,完全掌控组件代码。使用 CLI 添加组件,通过 CSS 变量自定义主题,配合 React Hook Form + Zod 构建表单,配合 TanStack Table 构建数据表格。

核心要点
  • shadcn/ui 不是组件库 — 它是你拥有的复制粘贴组件集合
  • 基于 Radix UI 原语实现无障碍,使用 Tailwind CSS 进行样式设计
  • 使用 CLI(npx shadcn@latest add)向项目添加组件
  • 通过 CSS 变量实现主题 — 开箱支持暗黑模式
  • 配合 React Hook Form + Zod 实现类型安全的表单验证
  • TanStack Table 集成实现强大灵活的数据表格
  • components.json 配置路径、样式和组件别名
  • 完全自定义 — 因为你拥有源代码,可以修改任何组件

什么是 shadcn/ui?

shadcn/ui 不是传统的组件库。你不需要在 package.json 中安装它。它提供 CLI 和组件注册表,你可以将组件直接复制到项目中,拥有每一行代码。

每个组件基于 Radix UI 构建 — 一个无头的、无样式的无障碍组件库 — 并用 Tailwind CSS 设计样式。

核心理念是:用你需要的,改你想改的。没有主题 API、没有包装组件、没有库更新带来的破坏性变更。

# The shadcn/ui approach:
# 1. You run the CLI to add a component
npx shadcn@latest add button

# 2. The component is copied into your project
# src/components/ui/button.tsx is created

# 3. You import and use it like any local component
import { Button } from "@/components/ui/button"

# 4. You own the code - edit it however you want
# No library dependency, no version conflicts

安装和设置

shadcn/ui 支持多种 React 框架。CLI 处理初始设置,包括创建 components.json 配置文件、设置 Tailwind CSS 和创建工具目录。

Next.js 安装

# Create a new Next.js project
npx create-next-app@latest my-app --typescript --tailwind --eslint
cd my-app

# Initialize shadcn/ui
npx shadcn@latest init

# The CLI will ask you:
# - Which style? (default / new-york)
# - Which color? (slate / gray / zinc / neutral / stone)
# - Use CSS variables for colors? (yes)

# Add your first component
npx shadcn@latest add button

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

Vite 安装

# Create a Vite + React + TypeScript project
npm create vite@latest my-app -- --template react-ts
cd my-app

# Install Tailwind CSS
npm install -D tailwindcss @tailwindcss/vite

# Update vite.config.ts to include the plugin
# import tailwindcss from "@tailwindcss/vite"
# plugins: [react(), tailwindcss()]

# Configure path aliases in tsconfig.json
# "paths": { "@/*": ["./src/*"] }

# Initialize shadcn/ui
npx shadcn@latest init

Remix 安装

# Create a Remix project with Vite
npx create-remix@latest my-app
cd my-app

# Install and configure Tailwind CSS
npm install -D tailwindcss @tailwindcss/vite

# Initialize shadcn/ui
npx shadcn@latest init

# Add components as needed
npx shadcn@latest add button dialog form

理解 components.json

components.json 是项目中 shadcn/ui 的核心配置文件。它告诉 CLI 在哪里放置组件、如何处理样式以及使用什么导入别名。

// components.json
{
  "\$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "src/app/globals.css",
    "baseColor": "zinc",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  }
}

关键字段:style 选择视觉风格,tailwind.config 指向 Tailwind 配置,tailwind.css 指向全局 CSS,aliases 映射导入路径。

使用组件

添加组件只需运行 CLI 命令。组件文件会被复制到 components.json 中指定的路径。

Button 按钮组件

Button 组件支持多种变体(default、destructive、outline、secondary、ghost、link)和尺寸(default、sm、lg、icon)。

// Install: npx shadcn@latest add button
import { Button } from "@/components/ui/button"

export function ButtonExamples() {
  return (
    <div>
      {/* Variants */}
      <Button variant="default">Default</Button>
      <Button variant="destructive">Delete</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="link">Link</Button>

      {/* Sizes */}
      <Button size="sm">Small</Button>
      <Button size="default">Default</Button>
      <Button size="lg">Large</Button>
      <Button size="icon"><IconComponent /></Button>

      {/* With loading state */}
      <Button disabled>
        <Loader2 style={{ animation: "spin 1s linear infinite" }} />
        Please wait
      </Button>
    </div>
  )
}

Dialog 对话框组件

Dialog 组件基于 Radix UI Dialog 构建,提供无障碍的模态对话框,处理焦点陷阱、键盘导航和屏幕阅读器播报。

// Install: npx shadcn@latest add dialog
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

export function DialogExample() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">Edit Profile</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Edit profile</DialogTitle>
          <DialogDescription>
            Make changes to your profile here.
          </DialogDescription>
        </DialogHeader>
        <div>
          <Label htmlFor="name">Name</Label>
          <Input id="name" defaultValue="John Doe" />
        </div>
        <DialogFooter>
          <Button type="submit">Save changes</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

Command 命令面板组件

Command 组件基于 cmdk 构建,提供可搜索的命令面板界面,支持键盘导航 — 适用于命令菜单、搜索对话框和组合框。

// Install: npx shadcn@latest add command
import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command"

export function CommandMenu() {
  return (
    <Command>
      <CommandInput placeholder="Type a command..." />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="Suggestions">
          <CommandItem>Calendar</CommandItem>
          <CommandItem>Search</CommandItem>
          <CommandItem>Calculator</CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Settings">
          <CommandItem>Profile</CommandItem>
          <CommandItem>Billing</CommandItem>
          <CommandItem>Settings</CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

Table 表格组件

Table 组件提供简洁的样式表格,支持表头、主体行、页脚和标题。

// Install: npx shadcn@latest add table
import {
  Table, TableBody, TableCaption,
  TableCell, TableHead, TableHeader, TableRow,
} from "@/components/ui/table"

export function TableExample() {
  return (
    <Table>
      <TableCaption>A list of recent invoices.</TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead>Invoice</TableHead>
          <TableHead>Status</TableHead>
          <TableHead>Method</TableHead>
          <TableHead>Amount</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        <TableRow>
          <TableCell>INV001</TableCell>
          <TableCell>Paid</TableCell>
          <TableCell>Credit Card</TableCell>
          <TableCell>\$250.00</TableCell>
        </TableRow>
      </TableBody>
    </Table>
  )
}

主题和暗黑模式

shadcn/ui 使用 CSS 变量进行主题设置,轻松创建自定义配色方案并支持暗黑模式。所有颜色使用 HSL 值定义。

/* globals.css - CSS Variables for theming */
@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;
    --popover: 0 0% 100%;
    --popover-foreground: 222.2 84% 4.9%;
    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;
    --secondary: 210 40% 96.1%;
    --secondary-foreground: 222.2 47.4% 11.2%;
    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;
    --accent: 210 40% 96.1%;
    --accent-foreground: 222.2 47.4% 11.2%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 210 40% 98%;
    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;
    --ring: 222.2 84% 4.9%;
    --radius: 0.5rem;
  }

  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --primary: 210 40% 98%;
    --primary-foreground: 222.2 47.4% 11.2%;
    --secondary: 217.2 32.6% 17.5%;
    --secondary-foreground: 210 40% 98%;
    --muted: 217.2 32.6% 17.5%;
    --muted-foreground: 215 20.2% 65.1%;
    --accent: 217.2 32.6% 17.5%;
    --accent-foreground: 210 40% 98%;
    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 210 40% 98%;
    --border: 217.2 32.6% 17.5%;
    --input: 217.2 32.6% 17.5%;
    --ring: 212.7 26.8% 83.9%;
  }
}

暗黑模式通过在 .dark 类中定义第二组 CSS 变量实现。切换 dark 类时,所有组件自动切换到暗黑模式颜色。

设置主题提供器

使用 next-themes(Next.js)或自定义提供器在明暗模式间切换。

// npm install next-themes
// components/theme-provider.tsx
"use client"

import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes"

export function ThemeProvider({
  children, ...props
}: ThemeProviderProps) {
  return (
    <NextThemesProvider {...props}>
      {children}
    </NextThemesProvider>
  )
}

// app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

CSS 变量自定义

因为你拥有组件代码,自定义很简单。修改 CSS 变量进行全局更改,或编辑单个组件文件进行特定更改。

CSS 变量方法意味着只需更改几个 HSL 值就能创建全新主题。命名约定使用语义名称如 background、foreground、primary 等。

/* Example: Creating a custom blue theme */
@layer base {
  :root {
    --primary: 221.2 83.2% 53.3%;    /* Blue */
    --primary-foreground: 210 40% 98%;
    --ring: 221.2 83.2% 53.3%;
    --radius: 0.75rem;  /* Rounder corners */
  }
}

/* Example: Creating a brand-colored theme */
@layer base {
  :root {
    /* Convert your brand color to HSL */
    /* Brand color: #6366f1 = 239 84% 67% */
    --primary: 239 84% 67%;
    --primary-foreground: 0 0% 100%;
    --ring: 239 84% 67%;
  }
}

React Hook Form + Zod 表单

shadcn/ui 提供强大的表单系统,集成 React Hook Form 进行状态管理和 Zod 进行基于模式的验证,实现类型安全的表单和自动错误处理。

构建表单的步骤:1) 定义 Zod 模式,2) 使用 useForm 和 zodResolver 创建表单,3) 使用 FormField 连接字段,4) 处理提交。

// Install dependencies:
// npx shadcn@latest add form input button
// npm install zod @hookform/resolvers

"use client"

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

// Step 1: Define Zod schema
const formSchema = z.object({
  username: z.string().min(2, {
    message: "Username must be at least 2 characters.",
  }),
  email: z.string().email({
    message: "Please enter a valid email address.",
  }),
})

// Step 2: Create the form component
export function ProfileForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
      email: "",
    },
  })

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

  // Step 3: Render form fields
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Username</FormLabel>
              <FormControl>
                <Input placeholder="shadcn" {...field} />
              </FormControl>
              <FormDescription>
                Your public display name.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  )
}

TanStack Table 数据表格

DataTable 组件将 shadcn/ui Table 与 TanStack Table 结合,提供排序、筛选、分页和行选择功能。

构建数据表格:1) 定义列,2) 创建使用 useReactTable 的 DataTable 组件,3) 使用 flexRender 渲染。

// Install: npx shadcn@latest add table
// npm install @tanstack/react-table

import {
  ColumnDef,
  flexRender,
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  SortingState,
  useReactTable,
} from "@tanstack/react-table"
import {
  Table, TableBody, TableCell,
  TableHead, TableHeader, TableRow,
} from "@/components/ui/table"

// Step 1: Define columns
type Payment = {
  id: string
  amount: number
  status: "pending" | "processing" | "success" | "failed"
  email: string
}

const columns: ColumnDef<Payment>[] = [
  {
    accessorKey: "status",
    header: "Status",
  },
  {
    accessorKey: "email",
    header: "Email",
  },
  {
    accessorKey: "amount",
    header: () => <div>Amount</div>,
    cell: ({ row }) => {
      const amount = parseFloat(row.getValue("amount"))
      const formatted = new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
      }).format(amount)
      return <div>{formatted}</div>
    },
  },
]

// Step 2: Create DataTable component
function DataTable<TData, TValue>({
  columns, data,
}: {
  columns: ColumnDef<TData, TValue>[]
  data: TData[]
}) {
  const [sorting, setSorting] = React.useState<SortingState>([])
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    onSortingChange: setSorting,
    getSortedRowModel: getSortedRowModel(),
    state: { sorting },
  })

  return (
    <Table>
      <TableHeader>
        {table.getHeaderGroups().map((hg) => (
          <TableRow key={hg.id}>
            {hg.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>
  )
}

无障碍特性

基于 Radix UI 原语构建,每个组件都遵循 WAI-ARIA 指南,包括正确的角色、键盘导航、焦点管理和屏幕阅读器支持。

  • 所有交互组件支持完整键盘导航(Tab、Enter、Space、方向键、Escape)
  • 焦点在模态对话框内被捕获,关闭时返回触发元素
  • ARIA 属性自动管理
  • 屏幕阅读器播报组件状态变化
  • 默认主题的颜色对比度符合 WCAG 2.1 AA 标准
  • 组件处理触摸目标和移动设备的响应行为
// Accessibility is built-in with Radix UI
// Example: Dialog with proper focus management
<Dialog>
  <DialogTrigger asChild>
    {/* Trigger receives focus on close */}
    <Button>Open Dialog</Button>
  </DialogTrigger>
  <DialogContent>
    {/* Focus is trapped inside */}
    {/* Escape key closes the dialog */}
    {/* aria-describedby auto-set */}
    <DialogTitle>Accessible Dialog</DialogTitle>
    <DialogDescription>
      Screen readers announce this description.
    </DialogDescription>
    <Button>First focusable element</Button>
  </DialogContent>
</Dialog>

创建自定义组件

你可以按照 shadcn/ui 模式创建自己的组件:使用 Radix UI 处理行为,Tailwind CSS 处理样式,cn() 工具处理条件类名。

// Example: Custom StatusBadge component
// following the shadcn/ui pattern
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const statusBadgeVariants = cva(
  "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring",
  {
    variants: {
      status: {
        online: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
        offline: "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300",
        busy: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
        away: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
      },
    },
    defaultVariants: {
      status: "offline",
    },
  }
)

interface StatusBadgeProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof statusBadgeVariants> {}

function StatusBadge({ status, ...props }: StatusBadgeProps) {
  return (
    <div
      {...props}
      // cn() merges classes without conflicts
    />
  )
}

export { StatusBadge, statusBadgeVariants }

Tailwind CSS 集成

shadcn/ui 与 Tailwind CSS 无缝配合。cn() 工具函数(基于 clsx 和 tailwind-merge)是组合类名的关键。

tailwind-merge 智能解决类名冲突,确保自定义类正确覆盖组件默认类。

// src/lib/utils.ts - The cn() utility
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

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

// Usage: combining component + custom classes
import { cn } from "@/lib/utils"

// Without cn(): "bg-primary bg-red-500"
// Both classes applied = unpredictable result

// With cn(): "bg-red-500"
// tailwind-merge resolves the conflict

function MyComponent({ isActive }: { isActive: boolean }) {
  return (
    <div
      // cn() handles conditional classes cleanly
    >
      Content
    </div>
  )
}

从其他 UI 库迁移

从 Material UI、Chakra UI 或 Ant Design 迁移时,shadcn/ui 支持渐进式迁移,可以逐个组件替换。

迁移策略:1) 并行安装,2) 从简单组件开始替换,3) 迁移表单组件,4) 最后迁移复杂组件,5) 移除旧库。

Featureshadcn/uiMaterial UIChakra UI
InstallationCopy to projectnpm dependencynpm dependency
StylingTailwind CSSEmotion / styledEmotion / styled
Code ownershipFull ownershipLibrary-ownedLibrary-owned
Bundle sizeOnly what you useTree-shakeableTree-shakeable
CustomizationEdit source directlyTheme API + sx propTheme API + style props
AccessibilityRadix UI (WAI-ARIA)Built-in (WAI-ARIA)Built-in (WAI-ARIA)
Update strategyManual (re-run CLI)npm updatenpm update
Dark modeCSS variablesThemeProviderColorModeProvider

最佳实践

  • 使用 CLI 添加组件 — 不要从 GitHub 手动复制
  • 将 components.json 纳入版本控制
  • 使用 cn() 工具处理所有条件类名逻辑
  • 在 globals.css 中自定义 CSS 变量以保持主题一致
  • 除非理解无障碍影响,否则不要修改 Radix UI 原语行为
  • 所有表单使用 React Hook Form + Zod
  • 用自己的组件包装 shadcn/ui 组件以设置项目默认值
  • 保持组件目录整洁 — 将相关组件分组到子目录
  • 定期重新运行 CLI add 命令更新组件
  • 使用 new-york 样式变体获得更精致紧凑的设计

常见问题

shadcn/ui 是组件库吗?

不是传统意义上的组件库。它是你复制粘贴到项目中的可复用组件集合,不作为 npm 依赖安装。你拥有代码,可以自定义一切。

如何更新 shadcn/ui 组件?

由于组件被复制到项目中,更新是手动的。重新运行 CLI add 命令,然后审查差异以合并更改。

可以不用 Tailwind CSS 使用 shadcn/ui 吗?

shadcn/ui 专为 Tailwind CSS 设计。如果不用 Tailwind,建议直接使用 Radix UI 配合你自己的样式方案。

shadcn/ui 可以用于生产环境吗?

可以,shadcn/ui 广泛用于生产环境。组件基于经过实战检验的 Radix UI 原语构建。

shadcn/ui 与 Material UI 或 Chakra UI 有何不同?

MUI 和 Chakra 作为依赖安装并提供自己的样式系统。shadcn/ui 使用 Tailwind CSS 并给你实际的源代码,提供更多控制和灵活性。

shadcn/ui 支持 React Server Components 吗?

静态组件如 Table、Badge 和 Card 可以作为服务器组件。交互组件需要 use client 指令。

可以在 Vue 或 Svelte 中使用 shadcn/ui 吗?

shadcn/ui 专为 React 构建,但社区已移植到 Vue(shadcn-vue)和 Svelte(shadcn-svelte)。

如何处理 shadcn/ui 中的组件依赖?

添加依赖其他组件的组件时,CLI 会自动安装依赖。例如添加 Combobox 会自动安装 Popover、Command 和 Button。

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

{ }JSON Formatter🌈CSS Gradient Generator

相关文章

Tailwind CSS指南:工具类、暗黑模式、v4和React/Next.js集成

掌握Tailwind CSS。涵盖工具类优先方法、响应式设计、flexbox/grid工具类、暗黑模式、自定义配置、Tailwind v4变化、React/Next.js与shadcn/ui集成以及Tailwind vs Bootstrap对比。

React Hooks 完全指南:useState、useEffect 和自定义 Hooks

通过实际示例掌握 React Hooks。学习 useState、useEffect、useContext、useReducer、useMemo、useCallback、自定义 Hooks 和 React 18+ 并发 Hooks。

Next.js App Router: 2026 完整迁移指南

掌握 Next.js App Router 的全面指南。学习 Server Components、数据获取、布局、流式渲染、Server Actions,以及从 Pages Router 的逐步迁移策略。