OA0 = Omni AI 0
OA0 是一个探索 AI 的论坛
现在注册
已注册用户请  登录
OA0  ›  技能包  ›  shadcn-ui: 使用 shadcn/ui 组件与 Tailwind CSS 构建 UI

shadcn-ui: 使用 shadcn/ui 组件与 Tailwind CSS 构建 UI

 
  nosql ·  2026-02-01 22:31:50 · 3 次点击  · 0 条评论  

name: shadcn-ui
version: 1.0.0
description: 当使用 shadcn/ui 组件构建 UI、Tailwind CSS 布局、结合 react-hook-form 与 zod 的表单模式、主题化、深色模式、侧边栏布局、移动端导航,或任何关于 shadcn 组件的问题时使用。
triggers:
- shadcn
- shadcn/ui
- radix
- 组件库
- UI 组件
- 表单模式
- react-hook-form
- 深色模式
- 主题化
- 侧边栏布局
- 对话框
- 抽屉
- 通知
- 下拉菜单
- 命令面板
- 数据表格
role: 专家
scope: 实现
output-format: 代码


shadcn/ui 专家指南

使用 shadcn/ui、Tailwind CSS、react-hook-form 和 zod 构建生产级 UI 的完整指南。

核心概念

shadcn/ui 不是一个组件库——它是一套基于 Radix UI 原语构建的、可复制粘贴的组件集合。你拥有这些代码。组件是添加到你的项目中的,而不是作为依赖项安装的。

安装

# 在 Next.js 项目中初始化 shadcn/ui
npx shadcn@latest init

# 添加单个组件
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog
npx shadcn@latest add form
npx shadcn@latest add input
npx shadcn@latest add select
npx shadcn@latest add table
npx shadcn@latest add toast
npx shadcn@latest add dropdown-menu
npx shadcn@latest add sheet
npx shadcn@latest add tabs
npx shadcn@latest add sidebar

# 一次性添加多个组件
npx shadcn@latest add button card input label textarea select checkbox

组件分类与使用场景

布局与导航

组件 使用场景
sidebar 带有可折叠区块的应用级导航
navigation-menu 带有下拉菜单的顶级网站导航
breadcrumb 显示页面层级/当前位置
tabs 在同一上下文中切换相关视图
separator 内容区块之间的视觉分隔线
sheet 侧滑面板(移动端导航、筛选器、详情视图)
resizable 可调整大小的面板布局

表单与输入

组件 使用场景
form 任何需要验证的表单(包装 react-hook-form)
input 文本、邮箱、密码、数字输入
textarea 多行文本输入
select 从列表中选择(类原生体验)
combobox 可搜索的选择器(使用 command + popover
checkbox 布尔值或多选开关
radio-group 从少量选项中进行单选
switch 开/关切换(设置、偏好)
slider 数值范围选择
date-picker 日期选择(使用 calendar + popover
toggle 按下/未按下状态(工具栏按钮)

反馈与浮层

组件 使用场景
dialog 模态确认框、表单或详情视图
alert-dialog 破坏性操作确认(“确定要删除吗?”)
sheet 侧边面板,用于表单、筛选器、移动端导航
toast 短暂的非阻塞性通知(通过 sonner
alert 内联状态消息(信息、警告、错误)
tooltip 图标/按钮的悬停提示
popover 点击时显示的富内容(颜色选择器、日期选择器)
hover-card 悬停时预览内容(用户资料、链接)
skeleton 加载占位符
progress 任务完成进度指示器

数据展示

组件 使用场景
table 表格数据展示
data-table 支持排序、筛选、分页的表格(使用 @tanstack/react-table
card 带有头部、主体、底部的卡片容器
badge 状态标签、徽章、计数
avatar 用户头像
accordion 可折叠的 FAQ 或设置区块
carousel 图片/内容轮播
scroll-area 自定义可滚动容器

操作

组件 使用场景
button 主要操作、表单提交
dropdown-menu 上下文菜单、操作菜单
context-menu 右键菜单
menubar 应用菜单栏
command 命令面板 / 搜索(⌘K)

表单模式 (react-hook-form + zod)

完整表单示例

npx shadcn@latest add form input select textarea checkbox button
'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'
import { Textarea } from '@/components/ui/textarea'
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select'
import { Checkbox } from '@/components/ui/checkbox'
import { toast } from 'sonner'

const formSchema = z.object({
  name: z.string().min(2, '姓名至少需要 2 个字符'),
  email: z.string().email('邮箱地址无效'),
  role: z.enum(['admin', 'user', 'editor'], { required_error: '请选择一个角色' }),
  bio: z.string().max(500).optional(),
  notifications: z.boolean().default(false),
})

type FormValues = z.infer<typeof formSchema>

export function UserForm() {
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: '',
      email: '',
      bio: '',
      notifications: false,
    },
  })

  async function onSubmit(values: FormValues) {
    try {
      await createUser(values)
      toast.success('用户创建成功')
      form.reset()
    } catch (error) {
      toast.error('用户创建失败')
    }
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>姓名</FormLabel>
              <FormControl>
                <Input placeholder="张三" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>邮箱</FormLabel>
              <FormControl>
                <Input type="email" placeholder="zhangsan@example.com" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="role"
          render={({ field }) => (
            <FormItem>
              <FormLabel>角色</FormLabel>
              <Select onValueChange={field.onChange} defaultValue={field.value}>
                <FormControl>
                  <SelectTrigger>
                    <SelectValue placeholder="选择一个角色" />
                  </SelectTrigger>
                </FormControl>
                <SelectContent>
                  <SelectItem value="admin">管理员</SelectItem>
                  <SelectItem value="editor">编辑</SelectItem>
                  <SelectItem value="user">用户</SelectItem>
                </SelectContent>
              </Select>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="bio"
          render={({ field }) => (
            <FormItem>
              <FormLabel>个人简介</FormLabel>
              <FormControl>
                <Textarea placeholder="介绍一下你自己..." {...field} />
              </FormControl>
              <FormDescription>最多 500 个字符</FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="notifications"
          render={({ field }) => (
            <FormItem className="flex flex-row items-start space-x-3 space-y-0">
              <FormControl>
                <Checkbox checked={field.value} onCheckedChange={field.onChange} />
              </FormControl>
              <div className="space-y-1 leading-none">
                <FormLabel>邮件通知</FormLabel>
                <FormDescription>接收关于账户活动的邮件</FormDescription>
              </div>
            </FormItem>
          )}
        />

        <Button type="submit" disabled={form.formState.isSubmitting}>
          {form.formState.isSubmitting ? '创建中...' : '创建用户'}
        </Button>
      </form>
    </Form>
  )
}

使用服务端操作的表单

'use client'

import { useFormState } from 'react-dom'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'

export function ContactForm() {
  const form = useForm<FormValues>({
    resolver: zodResolver(schema),
  })

  async function onSubmit(values: FormValues) {
    const formData = new FormData()
    Object.entries(values).forEach(([key, value]) => formData.append(key, String(value)))
    await submitContact(formData)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        {/* 字段 */}
      </form>
    </Form>
  )
}

主题化与深色模式

使用 next-themes 进行设置

npm install next-themes
npx shadcn@latest add dropdown-menu
// app/providers.tsx
'use client'
import { ThemeProvider } from 'next-themes'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
      {children}
    </ThemeProvider>
  )
}
// components/theme-toggle.tsx
'use client'
import { Moon, Sun } from 'lucide-react'
import { useTheme } from 'next-themes'
import { Button } from '@/components/ui/button'
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'

export function ThemeToggle() {
  const { setTheme } = useTheme()
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">切换主题</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme('light')}>浅色</DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme('dark')}>深色</DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme('system')}>跟随系统</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

globals.css 中自定义颜色

@layer base {
  :root {
    --background: 0 0% 100%;
    --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%;
    --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%;
    /* ... 其他变量 */
  }
}

常用布局

带侧边栏的应用外壳

import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
import { AppSidebar } from '@/components/app-sidebar'

export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <SidebarProvider>
      <AppSidebar />
      <main className="flex-1">
        <header className="flex h-14 items-center gap-4 border-b px-6">
          <SidebarTrigger />
          <h1 className="text-lg font-semibold">仪表盘</h1>
        </header>
        <div className="p-6">{children}</div>
      </main>
    </SidebarProvider>
  )
}

响应式头部与移动端导航

import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
import { Button } from '@/components/ui/button'
import { Menu } from 'lucide-react'

export function Header() {
  return (
    <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur">
      <div className="container flex h-14 items-center">
        <div className="mr-4 hidden md:flex">
          <Logo />
          <nav className="flex items-center gap-6 text-sm ml-6">
            <Link href="/dashboard">仪表盘</Link>
            <Link href="/settings">设置</Link>
          </nav>
        </div>

        {/* 移动端汉堡菜单 */}
        <Sheet>
          <SheetTrigger asChild>
            <Button variant="outline" size="icon" className="md:hidden">
              <Menu className="h-5 w-5" />
            </Button>
          </SheetTrigger>
          <SheetContent side="left" className="w-[300px]">
            <nav className="flex flex-col gap-4 mt-8">
              <Link href="/dashboard">仪表盘</Link>
              <Link href="/settings">设置</Link>
            </nav>
          </SheetContent>
        </Sheet>

        <div className="flex flex-1 items-center justify-end gap-2">
          <ThemeToggle />
          <UserMenu />
        </div>
      </div>
    </header>
  )
}

卡片网格

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'

export function StatsGrid({ stats }: { stats: Stat[] }) {
  return (
    <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
      {stats.map((stat) => (
        <Card key={stat.label}>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">{stat.label}</CardTitle>
            <stat.icon className="h-4 w-4 text-muted-foreground" />
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">{stat.value}</div>
            <p className="text-xs text-muted-foreground">{stat.description}</p>
          </CardContent>
        </Card>
      ))}
    </div>
  )
}

Tailwind CSS 模式

常用工具类模式

```tsx
// 居中

// 带最大宽度的容器
// 响应式网格
// 粘性头部
// 截断文本

非常长的文本...

// 多行截断

多行截断文本...

// 宽高比
// 动画
{/* 加载骨架屏 */}
{/* 旋转动画 */}
3 次点击  ∙  0 人收藏  
登录后收藏  
目前尚无回复
0 条回复
About   ·   Help   ·    
OA0 - Omni AI 0 一个探索 AI 的社区
沪ICP备2024103595号-2
Developed with Cursor