shadcn-ui
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chineseshadcn/ui Component Patterns
shadcn/ui 组件模式
Expert guide for building accessible, customizable UI components with shadcn/ui, Radix UI, and Tailwind CSS.
基于shadcn/ui、Radix UI和Tailwind CSS构建可访问、可自定义UI组件的专家指南。
Table of Contents
目录
When to Use
适用场景
- Setting up a new project with shadcn/ui
- Installing or configuring individual components
- Building forms with React Hook Form and Zod validation
- Creating accessible UI components (buttons, dialogs, dropdowns, sheets)
- Customizing component styling with Tailwind CSS
- Implementing design systems with shadcn/ui
- Building Next.js applications with TypeScript
- Creating complex layouts and data displays
- 使用shadcn/ui搭建新项目
- 安装或配置单个组件
- 使用React Hook Form和Zod验证构建表单
- 创建可访问的UI组件(按钮、对话框、下拉菜单、侧边面板)
- 使用Tailwind CSS自定义组件样式
- 基于shadcn/ui实现设计系统
- 使用TypeScript构建Next.js应用
- 创建复杂布局和数据展示
Quick Start
快速开始
For new projects, use the automated setup:
bash
undefined对于新项目,使用自动化设置:
bash
undefinedCreate Next.js project with shadcn/ui
Create Next.js project with shadcn/ui
npx create-next-app@latest my-app --typescript --tailwind --eslint --app
cd my-app
npx shadcn@latest init
npx create-next-app@latest my-app --typescript --tailwind --eslint --app
cd my-app
npx shadcn@latest init
Install essential components
Install essential components
npx shadcn@latest add button input form card dialog select
For existing projects:
```bashnpx shadcn@latest add button input form card dialog select
对于现有项目:
```bashInstall dependencies
Install dependencies
npm install tailwindcss-animate class-variance-authority clsx tailwind-merge lucide-react
npm install tailwindcss-animate class-variance-authority clsx tailwind-merge lucide-react
Initialize shadcn/ui
Initialize shadcn/ui
npx shadcn@latest init
undefinednpx shadcn@latest init
undefinedWhat is shadcn/ui?
什么是shadcn/ui?
shadcn/ui is not a traditional component library or npm package. Instead:
- It's a collection of reusable components that you can copy into your project
- Components are yours to customize - you own the code
- Built with Radix UI primitives for accessibility
- Styled with Tailwind CSS utilities
- Includes CLI tool for easy component installation
shadcn/ui 不是传统的组件库或npm包。而是:
- 它是一个可复用组件集合,你可以将其复制到自己的项目中
- 组件由你自定义 - 你拥有代码所有权
- 基于Radix UI原语构建,保障可访问性
- 使用Tailwind CSS工具类进行样式设计
- 包含CLI工具,便于组件安装
Installation & Setup
安装与设置
Initial Setup
初始设置
bash
undefinedbash
undefinedInitialize shadcn/ui in your project
Initialize shadcn/ui in your project
npx shadcn@latest init
During setup, you'll configure:
- TypeScript or JavaScript
- Style (Default, New York, etc.)
- Base color theme
- CSS variables or Tailwind CSS classes
- Component installation pathnpx shadcn@latest init
设置过程中,你需要配置:
- TypeScript 或 JavaScript
- 样式风格(默认、纽约等)
- 基础颜色主题
- CSS变量或Tailwind CSS类
- 组件安装路径Installing Individual Components
安装单个组件
bash
undefinedbash
undefinedInstall a single component
Install a single component
npx shadcn@latest add button
npx shadcn@latest add button
Install multiple components
Install multiple components
npx shadcn@latest add button input form
npx shadcn@latest add button input form
Install all components
Install all components
npx shadcn@latest add --all
undefinednpx shadcn@latest add --all
undefinedManual Installation
手动安装
If you prefer manual setup:
bash
undefined如果你偏好手动设置:
bash
undefinedInstall dependencies for a specific component
Install dependencies for a specific component
npm install @radix-ui/react-slot
npm install @radix-ui/react-slot
Copy component code from ui.shadcn.com
Copy component code from ui.shadcn.com
Place in src/components/ui/
Place in src/components/ui/
undefinedundefinedProject Configuration
项目配置
Required Dependencies
必需依赖
json
{
"dependencies": {
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-react": "^0.294.0",
"tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7"
}
}json
{
"dependencies": {
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-react": "^0.294.0",
"tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7"
}
}TSConfig Configuration
TSConfig 配置
json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/components/*": ["./src/components/*"],
"@/lib/*": ["./src/lib/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/components/*": ["./src/components/*"],
"@/lib/*": ["./src/lib/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}Tailwind Configuration
Tailwind 配置
js
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}js
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}CSS Variables (globals.css)
CSS 变量(globals.css)
css
@tailwind base;
@tailwind components;
@tailwind utilities;
@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%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-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%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}css
@tailwind base;
@tailwind components;
@tailwind utilities;
@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%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-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%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}Core Components
核心组件
Button Component
按钮组件
Installation:
bash
npx shadcn@latest add buttonBasic usage:
tsx
import { Button } from "@/components/ui/button";
export function ButtonDemo() {
return <Button>Click me</Button>;
}Button variants:
tsx
import { Button } from "@/components/ui/button";
export function ButtonVariants() {
return (
<div className="flex gap-4">
<Button variant="default">Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
</div>
);
}Button sizes:
tsx
<div className="flex gap-4 items-center">
<Button size="default">Default</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon">
<Icon className="h-4 w-4" />
</Button>
</div>With loading state:
tsx
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
export function ButtonLoading() {
return (
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Please wait
</Button>
);
}安装:
bash
npx shadcn@latest add button基础用法:
tsx
import { Button } from "@/components/ui/button";
export function ButtonDemo() {
return <Button>Click me</Button>;
}按钮变体:
tsx
import { Button } from "@/components/ui/button";
export function ButtonVariants() {
return (
<div className="flex gap-4">
<Button variant="default">Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
</div>
);
}按钮尺寸:
tsx
<div className="flex gap-4 items-center">
<Button size="default">Default</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon">
<Icon className="h-4 w-4" />
</Button>
</div>加载状态:
tsx
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
export function ButtonLoading() {
return (
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Please wait
</Button>
);
}Input & Form Fields
输入框与表单字段
Input Component
输入框组件
Installation:
bash
npx shadcn@latest add inputBasic input:
tsx
import { Input } from "@/components/ui/input";
export function InputDemo() {
return <Input type="email" placeholder="Email" />;
}Input with label:
tsx
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function InputWithLabel() {
return (
<div className="grid w-full max-w-sm items-center gap-1.5">
<Label htmlFor="email">Email</Label>
<Input type="email" id="email" placeholder="Email" />
</div>
);
}Input with button:
tsx
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export function InputWithButton() {
return (
<div className="flex w-full max-w-sm items-center gap-2">
<Input type="email" placeholder="Email" />
<Button type="submit" variant="outline">Subscribe</Button>
</div>
);
}安装:
bash
npx shadcn@latest add input基础输入框:
tsx
import { Input } from "@/components/ui/input";
export function InputDemo() {
return <Input type="email" placeholder="Email" />;
}带标签的输入框:
tsx
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function InputWithLabel() {
return (
<div className="grid w-full max-w-sm items-center gap-1.5">
<Label htmlFor="email">Email</Label>
<Input type="email" id="email" placeholder="Email" />
</div>
);
}带按钮的输入框:
tsx
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export function InputWithButton() {
return (
<div className="flex w-full max-w-sm items-center gap-2">
<Input type="email" placeholder="Email" />
<Button type="submit" variant="outline">Subscribe</Button>
</div>
);
}Forms with Validation
带验证的表单
Installation:
bash
npx shadcn@latest add formThis installs React Hook Form, Zod, and form components.
Complete form example:
tsx
"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,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { toast } from "@/components/ui/use-toast"
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.",
}),
})
export function ProfileForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(values, null, 2)}</code>
</pre>
),
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="you@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}安装:
bash
npx shadcn@latest add form这会安装React Hook Form、Zod和表单组件。
完整表单示例:
tsx
"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,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { toast } from "@/components/ui/use-toast"
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.",
}),
})
export function ProfileForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(values, null, 2)}</code>
</pre>
),
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="you@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}Card Component
卡片组件
Installation:
bash
npx shadcn@latest add cardBasic card:
tsx
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
export function CardDemo() {
return (
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card Description</CardDescription>
</CardHeader>
<CardContent>
<p>Card Content</p>
</CardContent>
<CardFooter>
<p>Card Footer</p>
</CardFooter>
</Card>
)
}Card with form:
tsx
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export function CardWithForm() {
return (
<Card className="w-[350px]">
<CardHeader>
<CardTitle>Create project</CardTitle>
<CardDescription>Deploy your new project in one-click.</CardDescription>
</CardHeader>
<CardContent>
<form>
<div className="grid w-full items-center gap-4">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="Name of your project" />
</div>
</div>
</form>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">Cancel</Button>
<Button>Deploy</Button>
</CardFooter>
</Card>
)
}安装:
bash
npx shadcn@latest add card基础卡片:
tsx
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
export function CardDemo() {
return (
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card Description</CardDescription>
</CardHeader>
<CardContent>
<p>Card Content</p>
</CardContent>
<CardFooter>
<p>Card Footer</p>
</CardFooter>
</Card>
)
}带表单的卡片:
tsx
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export function CardWithForm() {
return (
<Card className="w-[350px]">
<CardHeader>
<CardTitle>Create project</CardTitle>
<CardDescription>Deploy your new project in one-click.</CardDescription>
</CardHeader>
<CardContent>
<form>
<div className="grid w-full items-center gap-4">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="Name of your project" />
</div>
</div>
</form>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">Cancel</Button>
<Button>Deploy</Button>
</CardFooter>
</Card>
)
}Dialog (Modal) Component
对话框(模态框)组件
Installation:
bash
npx shadcn@latest add dialogBasic dialog:
tsx
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
export function DialogDemo() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open Dialog</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" value="Pedro Duarte" className="col-span-3" />
</div>
</div>
<DialogFooter>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}安装:
bash
npx shadcn@latest add dialog基础对话框:
tsx
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
export function DialogDemo() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open Dialog</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" value="Pedro Duarte" className="col-span-3" />
</div>
</div>
<DialogFooter>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}Sheet (Slide-over) Component
侧边面板(滑入式)组件
Installation:
bash
npx shadcn@latest add sheetBasic sheet:
tsx
import { Button } from "@/components/ui/button"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
export function SheetDemo() {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">Open Sheet</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Edit profile</SheetTitle>
<SheetDescription>
Make changes to your profile here. Click save when you're done.
</SheetDescription>
</SheetHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" value="Pedro Duarte" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="username" className="text-right">
Username
</Label>
<Input id="username" value="@peduarte" className="col-span-3" />
</div>
</div>
</SheetContent>
</Sheet>
)
}Sheet with side placement:
tsx
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">Open Right Sheet</Button>
</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Settings</SheetTitle>
<SheetDescription>
Configure your application settings here.
</SheetDescription>
</SheetHeader>
{/* Settings content */}
</SheetContent>
</Sheet>安装:
bash
npx shadcn@latest add sheet基础侧边面板:
tsx
import { Button } from "@/components/ui/button"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
export function SheetDemo() {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">Open Sheet</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Edit profile</SheetTitle>
<SheetDescription>
Make changes to your profile here. Click save when you're done.
</SheetDescription>
</SheetHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" value="Pedro Duarte" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="username" className="text-right">
Username
</Label>
<Input id="username" value="@peduarte" className="col-span-3" />
</div>
</div>
</SheetContent>
</Sheet>
)
}右侧滑入的侧边面板:
tsx
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">Open Right Sheet</Button>
</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Settings</SheetTitle>
<SheetDescription>
Configure your application settings here.
</SheetDescription>
</SheetHeader>
{/* Settings content */}
</SheetContent>
</Sheet>Menubar & Navigation
菜单栏与导航
Menubar Component
菜单栏组件
Installation:
bash
npx shadcn@latest add menubarBasic menubar:
tsx
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from "@/components/ui/menubar"
export function MenubarDemo() {
return (
<Menubar>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem>
New Tab <MenubarShortcut>⌘T</MenubarShortcut>
</MenubarItem>
<MenubarItem>
New Window <MenubarShortcut>⌘N</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem>Share</MenubarItem>
<MenubarSeparator />
<MenubarItem>Print</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Edit</MenubarTrigger>
<MenubarContent>
<MenubarItem>
Undo <MenubarShortcut>⌘Z</MenubarShortcut>
</MenubarItem>
<MenubarItem>
Redo <MenubarShortcut>⌘Y</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>Find</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem>Search the web</MenubarItem>
<MenubarItem>Find...</MenubarItem>
<MenubarItem>Find Next</MenubarItem>
<MenubarItem>Find Previous</MenubarItem>
</MenubarSubContent>
</MenubarSub>
</MenubarContent>
</MenubarMenu>
</Menubar>
)
}安装:
bash
npx shadcn@latest add menubar基础菜单栏:
tsx
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from "@/components/ui/menubar"
export function MenubarDemo() {
return (
<Menubar>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem>
New Tab <MenubarShortcut>⌘T</MenubarShortcut>
</MenubarItem>
<MenubarItem>
New Window <MenubarShortcut>⌘N</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem>Share</MenubarItem>
<MenubarSeparator />
<MenubarItem>Print</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Edit</MenubarTrigger>
<MenubarContent>
<MenubarItem>
Undo <MenubarShortcut>⌘Z</MenubarShortcut>
</MenubarItem>
<MenubarItem>
Redo <MenubarShortcut>⌘Y</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>Find</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem>Search the web</MenubarItem>
<MenubarItem>Find...</MenubarItem>
<MenubarItem>Find Next</MenubarItem>
<MenubarItem>Find Previous</MenubarItem>
</MenubarSubContent>
</MenubarSub>
</MenubarContent>
</MenubarMenu>
</Menubar>
)
}Select (Dropdown) Component
选择器(下拉菜单)组件
Installation:
bash
npx shadcn@latest add selectBasic select:
tsx
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
export function SelectDemo() {
return (
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="orange">Orange</SelectItem>
</SelectContent>
</Select>
)
}Select in form:
tsx
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="user">User</SelectItem>
<SelectItem value="guest">Guest</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>安装:
bash
npx shadcn@latest add select基础选择器:
tsx
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
export function SelectDemo() {
return (
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="orange">Orange</SelectItem>
</SelectContent>
</Select>
)
}表单中的选择器:
tsx
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="user">User</SelectItem>
<SelectItem value="guest">Guest</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>Toast Notifications
提示通知组件
Installation:
bash
npx shadcn@latest add toastSetup toast provider in root layout:
tsx
import { Toaster } from "@/components/ui/toaster"
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Toaster />
</body>
</html>
)
}Using toast:
tsx
import { useToast } from "@/components/ui/use-toast"
import { Button } from "@/components/ui/button"
export function ToastDemo() {
const { toast } = useToast()
return (
<Button
onClick={() => {
toast({
title: "Scheduled: Catch up",
description: "Friday, February 10, 2023 at 5:57 PM",
})
}}
>
Show Toast
</Button>
)
}Toast variants:
tsx
// Success
toast({
title: "Success",
description: "Your changes have been saved.",
})
// Error
toast({
variant: "destructive",
title: "Error",
description: "Something went wrong.",
})
// With action
toast({
title: "Uh oh! Something went wrong.",
description: "There was a problem with your request.",
action: <ToastAction altText="Try again">Try again</ToastAction>,
})安装:
bash
npx shadcn@latest add toast在根布局中设置提示通知提供者:
tsx
import { Toaster } from "@/components/ui/toaster"
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Toaster />
</body>
</html>
)
}使用提示通知:
tsx
import { useToast } from "@/components/ui/use-toast"
import { Button } from "@/components/ui/button"
export function ToastDemo() {
const { toast } = useToast()
return (
<Button
onClick={() => {
toast({
title: "Scheduled: Catch up",
description: "Friday, February 10, 2023 at 5:57 PM",
})
}}
>
Show Toast
</Button>
)
}提示通知变体:
tsx
// Success
toast({
title: "Success",
description: "Your changes have been saved.",
})
// Error
toast({
variant: "destructive",
title: "Error",
description: "Something went wrong.",
})
// With action
toast({
title: "Uh oh! Something went wrong.",
description: "There was a problem with your request.",
action: <ToastAction altText="Try again">Try again</ToastAction>,
})Table Component
表格组件
Installation:
bash
npx shadcn@latest add tableBasic table:
tsx
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
const invoices = [
{ invoice: "INV001", status: "Paid", method: "Credit Card", amount: "$250.00" },
{ invoice: "INV002", status: "Pending", method: "PayPal", amount: "$150.00" },
]
export function TableDemo() {
return (
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.invoice}>
<TableCell className="font-medium">{invoice.invoice}</TableCell>
<TableCell>{invoice.status}</TableCell>
<TableCell>{invoice.method}</TableCell>
<TableCell className="text-right">{invoice.amount}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}安装:
bash
npx shadcn@latest add table基础表格:
tsx
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
const invoices = [
{ invoice: "INV001", status: "Paid", method: "Credit Card", amount: "$250.00" },
{ invoice: "INV002", status: "Pending", method: "PayPal", amount: "$150.00" },
]
export function TableDemo() {
return (
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.invoice}>
<TableCell className="font-medium">{invoice.invoice}</TableCell>
<TableCell>{invoice.status}</TableCell>
<TableCell>{invoice.method}</TableCell>
<TableCell className="text-right">{invoice.amount}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}Customization
自定义配置
Theming with CSS Variables
使用CSS变量主题化
shadcn/ui uses CSS variables for theming. Configure in :
globals.csscss
@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%;
--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%;
/* ... other dark mode variables */
}
}shadcn/ui使用CSS变量进行主题化。在中配置:
globals.csscss
@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%;
--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%;
/* ... other dark mode variables */
}
}Customizing Components
自定义组件
Since you own the code, customize directly:
tsx
// components/ui/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent",
// Add custom variant
custom: "bg-gradient-to-r from-purple-500 to-pink-500 text-white",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
// Add custom size
xl: "h-14 rounded-md px-10 text-lg",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }由于你拥有代码所有权,可以直接修改组件:
tsx
// components/ui/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent",
// Add custom variant
custom: "bg-gradient-to-r from-purple-500 to-pink-500 text-white",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
// Add custom size
xl: "h-14 rounded-md px-10 text-lg",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
</Comp>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }Next.js Integration
Next.js 集成
App Router Setup
App Router 设置
For Next.js 13+ with App Router, ensure components use directive:
"use client"tsx
// src/components/ui/button.tsx
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
// ... rest of component对于Next.js 13+的App Router,确保组件使用指令:
"use client"tsx
// src/components/ui/button.tsx
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
// ... rest of componentLayout Integration
布局集成
Add the Toaster to your root layout:
tsx
// app/layout.tsx
import { Toaster } from "@/components/ui/toaster"
import "./globals.css"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className="min-h-screen bg-background font-sans antialiased">
{children}
<Toaster />
</body>
</html>
)
}在根布局中添加Toaster:
tsx
// app/layout.tsx
import { Toaster } from "@/components/ui/toaster"
import "./globals.css"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className="min-h-screen bg-background font-sans antialiased">
{children}
<Toaster />
</body>
</html>
)
}Server Components
服务器组件
When using shadcn/ui components in Server Components, wrap them in a Client Component:
tsx
// app/dashboard/page.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ButtonClient } from "@/components/ui/button-client"
export default function DashboardPage() {
return (
<div className="container mx-auto p-6">
<Card>
<CardHeader>
<CardTitle>Dashboard</CardTitle>
</CardHeader>
<CardContent>
<ButtonClient>Interactive Button</ButtonClient>
</CardContent>
</Card>
</div>
)
}tsx
// src/components/ui/button-client.tsx
"use client"
import { Button } from "./button"
export function ButtonClient(props: React.ComponentProps<typeof Button>) {
return <Button {...props} />
}在服务器组件中使用shadcn/ui组件时,将其包裹在客户端组件中:
tsx
// app/dashboard/page.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ButtonClient } from "@/components/ui/button-client"
export default function DashboardPage() {
return (
<div className="container mx-auto p-6">
<Card>
<CardHeader>
<CardTitle>Dashboard</CardTitle>
</CardHeader>
<CardContent>
<ButtonClient>Interactive Button</ButtonClient>
</CardContent>
</Card>
</div>
)
}tsx
// src/components/ui/button-client.tsx
"use client"
import { Button } from "./button"
export function ButtonClient(props: React.ComponentProps<typeof Button>) {
return <Button {...props} />
}Route Handlers with Forms
带表单的路由处理器
Create API routes for form submissions:
tsx
// app/api/contact/route.ts
import { NextRequest, NextResponse } from "next/server"
import { z } from "zod"
const contactSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(10),
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validated = contactSchema.parse(body)
// Process form data
console.log("Form submission:", validated)
return NextResponse.json({ success: true })
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ errors: error.errors },
{ status: 400 }
)
}
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
)
}
}为表单提交创建API路由:
tsx
// app/api/contact/route.ts
import { NextRequest, NextResponse } from "next/server"
import { z } from "zod"
const contactSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(10),
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validated = contactSchema.parse(body)
// Process form data
console.log("Form submission:", validated)
return NextResponse.json({ success: true })
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ errors: error.errors },
{ status: 400 }
)
}
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
)
}
}Form with Server Action
使用Server Action的表单
Using Next.js 14+ Server Actions:
tsx
// app/contact/page.tsx
"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"
import { Textarea } from "@/components/ui/textarea"
import { toast } from "@/components/ui/use-toast"
const formSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(10),
})
async function onSubmit(values: z.infer<typeof formSchema>) {
try {
const response = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
})
if (!response.ok) throw new Error("Failed to submit")
toast({
title: "Success!",
description: "Your message has been sent.",
})
} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: "Failed to send message. Please try again.",
})
}
}
export default function ContactPage() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
})
return (
<div className="container mx-auto max-w-2xl py-8">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Your name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="your@email.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>Message</FormLabel>
<FormControl>
<Textarea
placeholder="Your message..."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Send Message
</Button>
</form>
</Form>
</div>
)
}使用Next.js 14+的Server Actions:
tsx
// app/contact/page.tsx
"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"
import { Textarea } from "@/components/ui/textarea"
import { toast } from "@/components/ui/use-toast"
const formSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(10),
})
async function onSubmit(values: z.infer<typeof formSchema>) {
try {
const response = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
})
if (!response.ok) throw new Error("Failed to submit")
toast({
title: "Success!",
description: "Your message has been sent.",
})
} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: "Failed to send message. Please try again.",
})
}
}
export default function ContactPage() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
})
return (
<div className="container mx-auto max-w-2xl py-8">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Your name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="your@email.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>Message</FormLabel>
<FormControl>
<Textarea
placeholder="Your message..."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Send Message
</Button>
</form>
</Form>
</div>
)
}Metadata with shadcn/ui
shadcn/ui 与元数据
Using shadcn/ui components in metadata:
tsx
// app/layout.tsx
import { Metadata } from "next"
export const metadata: Metadata = {
title: {
default: "My App",
template: "%s | My App",
},
description: "Built with shadcn/ui and Next.js",
}
// app/about/page.tsx
import { Metadata } from "next"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
export const metadata: Metadata = {
title: "About Us",
description: "Learn more about our company",
}
export default function AboutPage() {
return (
<div className="container mx-auto py-8">
<Card>
<CardHeader>
<CardTitle>About Our Company</CardTitle>
</CardHeader>
<CardContent>
<p>We build amazing products with modern web technologies.</p>
</CardContent>
</Card>
</div>
)
}在元数据中使用shadcn/ui组件:
tsx
// app/layout.tsx
import { Metadata } from "next"
export const metadata: Metadata = {
title: {
default: "My App",
template: "%s | My App",
},
description: "Built with shadcn/ui and Next.js",
}
// app/about/page.tsx
import { Metadata } from "next"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
export const metadata: Metadata = {
title: "About Us",
description: "Learn more about our company",
}
export default function AboutPage() {
return (
<div className="container mx-auto py-8">
<Card>
<CardHeader>
<CardTitle>About Our Company</CardTitle>
</CardHeader>
<CardContent>
<p>We build amazing products with modern web technologies.</p>
</CardContent>
</Card>
</div>
)
}Font Optimization
字体优化
Optimize fonts with next/font:
tsx
// app/layout.tsx
import { Inter } from "next/font/google"
import { Toaster } from "@/components/ui/toaster"
import { cn } from "@/lib/utils"
import "./globals.css"
const inter = Inter({ subsets: ["latin"] })
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className={cn("min-h-screen bg-background font-sans antialiased", inter.className)}>
{children}
<Toaster />
</body>
</html>
)
}使用next/font优化字体:
tsx
// app/layout.tsx
import { Inter } from "next/font/google"
import { Toaster } from "@/components/ui/toaster"
import { cn } from "@/lib/utils"
import "./globals.css"
const inter = Inter({ subsets: ["latin"] })
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className={cn("min-h-screen bg-background font-sans antialiased", inter.className)}>
{children}
<Toaster />
</body>
</html>
)
}Advanced Patterns
高级模式
Form with Multiple Fields
多字段表单
tsx
const formSchema = z.object({
username: z.string().min(2).max(50),
email: z.string().email(),
bio: z.string().max(160).min(4),
role: z.enum(["admin", "user", "guest"]),
notifications: z.boolean().default(false),
})
export function AdvancedForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
bio: "",
role: "user",
notifications: false,
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
{/* Username field */}
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="johndoe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Email field */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Textarea field */}
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder="Tell us about yourself"
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Select field */}
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="user">User</SelectItem>
<SelectItem value="guest">Guest</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* Checkbox field */}
<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>Email notifications</FormLabel>
<FormDescription>
Receive emails about your account activity.
</FormDescription>
</div>
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}tsx
const formSchema = z.object({
username: z.string().min(2).max(50),
email: z.string().email(),
bio: z.string().max(160).min(4),
role: z.enum(["admin", "user", "guest"]),
notifications: z.boolean().default(false),
})
export function AdvancedForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
bio: "",
role: "user",
notifications: false,
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
{/* Username field */}
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="johndoe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Email field */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Textarea field */}
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder="Tell us about yourself"
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Select field */}
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="user">User</SelectItem>
<SelectItem value="guest">Guest</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* Checkbox field */}
<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>Email notifications</FormLabel>
<FormDescription>
Receive emails about your account activity.
</FormDescription>
</div>
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}Best Practices
最佳实践
- Accessibility: Components use Radix UI primitives for ARIA compliance
- Customization: Modify components directly in your codebase
- Type Safety: Use TypeScript for type-safe props and state
- Validation: Use Zod schemas for form validation
- Styling: Leverage Tailwind utilities and CSS variables
- Consistency: Use the same component patterns across your app
- Testing: Components are testable with React Testing Library
- Performance: Components are optimized and tree-shakeable
- 可访问性:组件使用Radix UI原语,符合ARIA规范
- 自定义:直接在你的代码库中修改组件
- 类型安全:使用TypeScript实现类型安全的属性和状态
- 验证:使用Zod schema进行表单验证
- 样式:利用Tailwind工具类和CSS变量
- 一致性:在应用中使用统一的组件模式
- 可测试性:组件可通过React Testing Library进行测试
- 性能:组件经过优化,支持tree-shaking
Common Component Combinations
常见组件组合
Login Form
登录表单
tsx
<Card className="w-[350px]">
<CardHeader>
<CardTitle>Login</CardTitle>
<CardDescription>Enter your credentials to continue</CardDescription>
</CardHeader>
<CardContent>
<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 type="email" placeholder="you@example.com" {...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" className="w-full">Login</Button>
</form>
</Form>
</CardContent>
</Card>tsx
<Card className="w-[350px]">
<CardHeader>
<CardTitle>Login</CardTitle>
<CardDescription>Enter your credentials to continue</CardDescription>
</CardHeader>
<CardContent>
<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 type="email" placeholder="you@example.com" {...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" className="w-full">Login</Button>
</form>
</Form>
</CardContent>
</Card>References
参考资料
- Official Docs: https://ui.shadcn.com
- Radix UI: https://www.radix-ui.com
- React Hook Form: https://react-hook-form.com
- Zod: https://zod.dev
- Tailwind CSS: https://tailwindcss.com
- Examples: https://ui.shadcn.com/examples
- 官方文档: https://ui.shadcn.com
- Radix UI: https://www.radix-ui.com
- React Hook Form: https://react-hook-form.com
- Zod: https://zod.dev
- Tailwind CSS: https://tailwindcss.com
- 示例: https://ui.shadcn.com/examples