- 手风琴 (Accordion)
- 警告 (Alert)
- 警告对话框 (Alert Dialog)
- 宽高比 (Aspect Ratio)
- 头像 (Avatar)
- 徽章 (Badge)
- 面包屑 (Breadcrumb)
- 按钮 (Button)
- 按钮组 (Button Group)
- 日历 (Calendar)
- 卡片 (Card)
- 轮播图 (Carousel)
- 图表 (Chart)
- 复选框 (Checkbox)
- 折叠面板 (Collapsible)
- 组合框 (Combobox)
- 命令面板 (Command)
- 上下文菜单 (Context Menu)
- 数据表格 (Data Table)
- 日期选择器 (Date Picker)
- 对话框 (Dialog)
- 抽屉 (Drawer)
- 下拉菜单 (Dropdown Menu)
- 空状态 (Empty)
- 字段 (Field)
- 表单 (Form)
- 悬停卡片 (Hover Card)
- 输入框 (Input)
- 输入组 (Input Group)
- 验证码输入 (Input OTP)
- 项 (Item)
- 键盘按键 (Kbd)
- 标签 (Label)
- 菜单栏 (Menubar)
- 原生选择器 (Native Select)
- 导航菜单 (Navigation Menu)
- 数字输入框 (Number Field)
- 分页 (Pagination)
- 引脚输入 (Pin Input)
- 气泡卡片 (Popover)
- 进度条 (Progress)
- 单选框组 (Radio Group)
- 范围日历 (Range Calendar)
- 可调整大小 (Resizable)
- 滚动区域 (Scroll Area)
- 选择器 (Select)
- 分隔线 (Separator)
- 侧边栏抽屉 (Sheet)
- 侧边栏 (Sidebar)
- 骨架屏 (Skeleton)
- 滑块 (Slider)
- 轻量提示 (Sonner)
- 加载动画 (Spinner)
- 步骤条 (Stepper)
- 开关 (Switch)
- 表格 (Table)
- 标签页 (Tabs)
- 标签输入 (Tags Input)
- 文本域 (Textarea)
- 吐司提示 (Toast)
- 切换按钮 (Toggle)
- 切换按钮组 (Toggle Group)
- 工具提示 (Tooltip)
- 排版 (Typography)
2025 年 11 月 - 追赶进度
我们将 shadcn-vue.com 升级到了 Nuxt v4 和 Tailwind v4。网站现在使用升级后的 new-york 组件库。
我们还进行了一些小的设计更新,使网站运行更快、更易于导航。
图表 (Chart)
我们重构了 Chart 组件,使其与原始组件保持一致,并尽可能贴近原 API。
2025 年 10 月 - 新增组件
针对这轮组件更新,我审视了我们日常开发中的需求,那些枯燥、反复重写的模块,并将它们转化为你可以真正使用的可复用抽象组件。
- Spinner (加载指示器):用于显示加载状态的指示器。
- Kbd (键盘按键):用于展示键盘按键或按键组合。
- Button Group (按钮组):用于组合相关操作或拆分按钮的按钮组。
- Input Group (输入组):带有图标、按钮、标签等的输入框。
- Field (表单域):一个组件,搞定你的所有表单。
- Item (条目):用于展示列表项、卡片等。
- Empty (空状态):用于处理各种空状态场景。
加载指示器
好的,让我们从最简单的开始:Spinner 和 Kbd。非常基础,大家都知道它们的作用。
这是渲染 Spinner 的方式
<script setup lang="ts">
import { Spinner } from '@/components/ui/spinner'
</script>
<template>
<Spinner />
</template>这是它的视觉效果
<script setup lang="ts">
import { Spinner } from '@/components/ui/spinner'
</script>
<template>
<div class="flex flex-col items-center justify-center gap-8">
<Spinner />
</div>
</template>这是它在按钮中的效果
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/spinner'
</script>
<template>
<div class="flex flex-col items-center gap-4">
<Button disabled size="sm">
<Spinner />
Loading...
</Button>
<Button variant="outline" disabled size="sm">
<Spinner />
Please wait
</Button>
<Button variant="secondary" disabled size="sm">
<Spinner />
Processing
</Button>
</div>
</template>你可以编辑代码并将其替换为你自己的 Spinner。
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { LoaderIcon } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<LoaderIcon
role="status"
aria-label="Loading"
:class="cn('size-4 animate-spin', props.class)"
/>
</template>Kbd
Kbd 是一个渲染键盘按键的组件。
<script setup lang="ts">
import { Kbd, KbdGroup } from '@/components/ui/kbd'
</script>
<template>
<Kbd>Ctrl</Kbd>
</template>使用 KbdGroup 将多个按键组合在一起。
<template>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>B</Kbd>
</KbdGroup>
</template><script lang='ts' setup>
import { Kbd, KbdGroup } from '@/components/ui/kbd'
</script>
<template>
<div class="flex flex-col items-center gap-4">
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>⇧</Kbd>
<Kbd>⌥</Kbd>
<Kbd>⌃</Kbd>
</KbdGroup>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<span>+</span>
<Kbd>B</Kbd>
</KbdGroup>
</div>
</template>你可以将其添加到按钮、工具提示、输入组等组件中。
按钮组
我收到了很多关于这个组件的请求:Button Group(按钮组)。这是一个容器,用于将相关的按钮组合在一起,并保持样式统一。非常适合操作组、拆分按钮等场景。
<script setup lang="ts">
import { ArchiveIcon, ArrowLeftIcon, CalendarPlusIcon, ClockIcon, ListFilterPlusIcon, MailCheckIcon, MoreHorizontalIcon, TagIcon, Trash2Icon } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
const label = ref('personal')
</script>
<template>
<ButtonGroup>
<ButtonGroup class="hidden sm:flex">
<Button variant="outline" size="icon" aria-label="Go Back">
<ArrowLeftIcon />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">
Archive
</Button>
<Button variant="outline">
Report
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">
Snooze
</Button>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" size="icon" aria-label="More Options">
<MoreHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-52">
<DropdownMenuGroup>
<DropdownMenuItem>
<MailCheckIcon />
Mark as Read
</DropdownMenuItem>
<DropdownMenuItem>
<ArchiveIcon />
Archive
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<ClockIcon />
Snooze
</DropdownMenuItem>
<DropdownMenuItem>
<CalendarPlusIcon />
Add to Calendar
</DropdownMenuItem>
<DropdownMenuItem>
<ListFilterPlusIcon />
Add to List
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<TagIcon class="mr-2 size-4" />
Label As...
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup v-model="label">
<DropdownMenuRadioItem value="personal">
Personal
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="work">
Work
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="other">
Other
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
<Trash2Icon />
Trash
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</ButtonGroup>
</template>这是代码
<script setup lang="ts">
import { ButtonGroup } from '@/components/ui/button-group'
</script>
<template>
<ButtonGroup>
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>
</template>你可以嵌套按钮组,以创建带有间距的更复杂布局。
<template>
<ButtonGroup>
<ButtonGroup>
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>
<ButtonGroup>
<Button>Button 3</Button>
<Button>Button 4</Button>
</ButtonGroup>
</ButtonGroup>
</template>使用 ButtonGroupSeparator 来创建拆分按钮,这是经典的下拉菜单模式。
<script setup lang="ts">
import { AlertTriangleIcon, CheckIcon, ChevronDownIcon, CopyIcon, ShareIcon, TrashIcon, UserRoundXIcon, VolumeOffIcon } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
</script>
<template>
<ButtonGroup>
<Button variant="outline">
Follow
</Button>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" size="icon">
<ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="[--radius:1rem]">
<DropdownMenuGroup>
<DropdownMenuItem>
<VolumeOffIcon />
Mute Conversation
</DropdownMenuItem>
<DropdownMenuItem>
<CheckIcon />
Mark as Read
</DropdownMenuItem>
<DropdownMenuItem>
<AlertTriangleIcon />
Report Conversation
</DropdownMenuItem>
<DropdownMenuItem>
<UserRoundXIcon />
Block User
</DropdownMenuItem>
<DropdownMenuItem>
<ShareIcon />
Share Conversation
</DropdownMenuItem>
<DropdownMenuItem>
<CopyIcon />
Copy Conversation
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
<TrashIcon />
Delete Conversation
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</template>你还可以使用它为输入框添加前缀或后缀按钮及文本。
<script setup lang="ts">
import { ArrowRightIcon } from 'lucide-vue-next'
import { ref } from 'vue'
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
const CURRENCIES = [
{
value: '$',
label: 'US Dollar',
},
{
value: '€',
label: 'Euro',
},
{
value: '£',
label: 'British Pound',
},
]
const currency = ref('$')
</script>
<template>
<ButtonGroup>
<ButtonGroup>
<Select v-model="currency">
<SelectTrigger class="font-mono w-14">
{{ currency }}
</SelectTrigger>
<SelectContent class="min-w-24">
<SelectItem v-for="item in CURRENCIES" :key="item.value" :value="item.value">
{{ item.value }}
<span class="text-muted-foreground">{{ item.label }}</span>
</SelectItem>
</SelectContent>
</Select>
<Input placeholder="10.00" pattern="[0-9]*" />
</ButtonGroup>
<ButtonGroup>
<Button aria-label="Send" size="icon" variant="outline">
<ArrowRightIcon />
</Button>
</ButtonGroup>
</ButtonGroup>
</template><template>
<ButtonGroup>
<ButtonGroupText>Prefix</ButtonGroupText>
<Input placeholder="Type something here..." />
<Button>Button</Button>
</ButtonGroup>
</template>输入组
Input Group(输入组)允许你向输入框添加图标、按钮等。你知道的,就是那些你输入框周围总需要用到的零碎东西。
<script setup lang="ts">
import { InputGroup, InputGroupAddon, InputGroupInput } from '@/components/ui/input-group'
</script>
<template>
<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
</InputGroup>
</template>这是带图标的预览效果
<script setup lang="ts">
import { CheckIcon, CreditCardIcon, InfoIcon, MailIcon, SearchIcon, StarIcon } from 'lucide-vue-next'
import { InputGroup, InputGroupAddon, InputGroupInput } from '@/components/ui/input-group'
</script>
<template>
<div class="grid w-full max-w-sm gap-6">
<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput type="email" placeholder="Enter your email" />
<InputGroupAddon>
<MailIcon />
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="Card number" />
<InputGroupAddon>
<CreditCardIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<CheckIcon />
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="Card number" />
<InputGroupAddon align="inline-end">
<StarIcon />
<InfoIcon />
</InputGroupAddon>
</InputGroup>
</div>
</template>你也可以在输入组中添加按钮。
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'
import { CheckIcon, CopyIcon, InfoIcon, StarIcon } from 'lucide-vue-next'
import { ref } from 'vue'
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@/components/ui/input-group'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
const isFavorite = ref(false)
const source = ref('hello')
const { text, copy, copied, isSupported } = useClipboard({ source })
</script>
<template>
<div class="grid w-full max-w-sm gap-6">
<InputGroup>
<InputGroupInput placeholder="https://x.com/shadcn" read-only />
<InputGroupAddon align="inline-end">
<InputGroupButton
aria-label="Copy"
title="Copy"
size="icon-xs"
@click="copy('https://x.com/shadcn')"
>
<CheckIcon v-if="!copied" />
<CopyIcon v-if="copied" />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup class="[--radius:9999px]">
<Popover>
<PopoverTrigger as-child>
<InputGroupAddon>
<InputGroupButton variant="secondary" size="icon-xs">
<InfoIcon />
</InputGroupButton>
</InputGroupAddon>
</PopoverTrigger>
<PopoverContent
align="start"
class="flex flex-col gap-1 text-sm rounded-xl"
>
<p class="font-medium">
Your connection is not secure.
</p>
<p>You should not enter any sensitive information on this site.</p>
</PopoverContent>
</Popover>
<InputGroupAddon class="text-muted-foreground pl-1.5">
https://
</InputGroupAddon>
<InputGroupInput id="input-secure-19" />
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
@click="isFavorite = !isFavorite"
>
<StarIcon
data-favorite="{isFavorite}"
class="data-[favorite=true]:fill-blue-600 data-[favorite=true]:stroke-blue-600"
/>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="Type to search..." />
<InputGroupAddon align="inline-end">
<InputGroupButton variant="secondary">
Search
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
</template>或者文字、标签、工具提示……
<script setup lang="ts">
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText, InputGroupTextarea } from '@/components/ui/input-group'
</script>
<template>
<div class="grid w-full max-w-sm gap-6">
<InputGroup>
<InputGroupAddon>
<InputGroupText>$</InputGroupText>
</InputGroupAddon>
<InputGroupInput placeholder="0.00" />
<InputGroupAddon align="inline-end">
<InputGroupText>USD</InputGroupText>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupInput placeholder="example.com" class="!pl-0.5" />
<InputGroupAddon align="inline-end">
<InputGroupText>.com</InputGroupText>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="Enter your username" />
<InputGroupAddon align="inline-end">
<InputGroupText>@company.com</InputGroupText>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupTextarea placeholder="Enter your message" />
<InputGroupAddon align="block-end">
<InputGroupText class="text-xs text-muted-foreground">
120 characters left
</InputGroupText>
</InputGroupAddon>
</InputGroup>
</div>
</template>它也适用于文本域(textarea),因此你可以构建包含大量控制项的复杂组件,或是又一个提示输入表单。
<script setup lang="ts">
import { BracesIcon, CopyIcon, CornerDownLeftIcon, RefreshCwIcon } from 'lucide-vue-next'
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupTextarea } from '@/components/ui/input-group'
</script>
<template>
<div class="grid w-full max-w-md gap-4">
<InputGroup>
<InputGroupTextarea
id="textarea-code-32"
placeholder="console.log('Hello, world!');"
class="min-h-[200px]"
/>
<InputGroupAddon align="block-end" class="border-t">
<InputGroupText>Line 1, Column 1</InputGroupText>
<InputGroupButton size="sm" class="ml-auto" variant="default">
Run <CornerDownLeftIcon />
</InputGroupButton>
</InputGroupAddon>
<InputGroupAddon align="block-start" class="border-b">
<InputGroupText class="font-mono font-medium">
<BracesIcon />
script.js
</InputGroupText>
<InputGroupButton class="ml-auto" size="icon-xs">
<RefreshCwIcon />
</InputGroupButton>
<InputGroupButton variant="ghost" size="icon-xs">
<CopyIcon />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
</template>哦,这里还有一些带有 Spinner 的酷炫示例
<script setup lang="ts">
import { LoaderIcon } from 'lucide-vue-next'
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from '@/components/ui/input-group'
import { Spinner } from '@/components/ui/spinner'
</script>
<template>
<div class="grid w-full max-w-sm gap-4">
<InputGroup data-disabled>
<InputGroupInput placeholder="Searching..." disabled />
<InputGroupAddon align="inline-end">
<Spinner />
</InputGroupAddon>
</InputGroup>
<InputGroup data-disabled>
<InputGroupInput placeholder="Processing..." disabled />
<InputGroupAddon>
<Spinner />
</InputGroupAddon>
</InputGroup>
<InputGroup data-disabled>
<InputGroupInput placeholder="Saving changes..." disabled />
<InputGroupAddon align="inline-end">
<InputGroupText>Saving...</InputGroupText>
<Spinner />
</InputGroupAddon>
</InputGroup>
<InputGroup data-disabled>
<InputGroupInput placeholder="Refreshing data..." disabled />
<InputGroupAddon>
<LoaderIcon class="animate-spin" />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupText class="text-muted-foreground">
Please wait...
</InputGroupText>
</InputGroupAddon>
</InputGroup>
</div>
</template>Field (表单域)
隆重介绍 Field,一个用于构建极其复杂表单的组件。这里的抽象设计非常优美。
我花了很多时间才完善它,但我让它兼容了你所有的表单库:Vee Validate、TanStack Form 以及自定义表单方案。
这是一个带有输入框的基础表单域
<script setup lang="ts">
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from '@/components/ui/field'
</script>
<template>
<Field>
<FieldLabel html-for="username">
Username
</FieldLabel>
<Input id="username" placeholder="Max Leiter" />
<FieldDescription>
Choose a unique username for your account.
</FieldDescription>
</Field>
</template><script setup lang="ts">
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSet,
} from '@/components/ui/field'
import { Input } from '@/components/ui/input'
</script>
<template>
<div class="w-full max-w-md">
<FieldSet>
<FieldGroup>
<Field>
<FieldLabel for="username">
Username
</FieldLabel>
<Input id="username" type="text" placeholder="Max Leiter" />
<FieldDescription>
Choose a unique username for your account.
</FieldDescription>
</Field>
<Field>
<FieldLabel for="password">
Password
</FieldLabel>
<FieldDescription>
Must be at least 8 characters long.
</FieldDescription>
<Input id="password" type="password" placeholder="********" />
</Field>
</FieldGroup>
</FieldSet>
</div>
</template>它适用于所有表单控件:输入框、文本域、选择框、复选框、单选框、开关、滑块,应有尽有。这是一个完整示例
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
</script>
<template>
<div class="w-full max-w-md">
<form>
<FieldGroup>
<FieldSet>
<FieldLegend>Payment Method</FieldLegend>
<FieldDescription>
All transactions are secure and encrypted
</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel for="checkout-7j9-card-name-43j">
Name on Card
</FieldLabel>
<Input
id="checkout-7j9-card-name-43j"
placeholder="Evil Rabbit"
required
/>
</Field>
<Field>
<FieldLabel for="checkout-7j9-card-number-uw1">
Card Number
</FieldLabel>
<Input
id="checkout-7j9-card-number-uw1"
placeholder="1234 5678 9012 3456"
required
/>
<FieldDescription>
Enter your 16-digit card number
</FieldDescription>
</Field>
<div class="grid grid-cols-3 gap-4">
<Field>
<FieldLabel for="checkout-exp-month-ts6">
Month
</FieldLabel>
<Select default-value="">
<SelectTrigger id="checkout-exp-month-ts6">
<SelectValue placeholder="MM" />
</SelectTrigger>
<SelectContent>
<SelectItem value="01">
01
</SelectItem>
<SelectItem value="02">
02
</SelectItem>
<SelectItem value="03">
03
</SelectItem>
<SelectItem value="04">
04
</SelectItem>
<SelectItem value="05">
05
</SelectItem>
<SelectItem value="06">
06
</SelectItem>
<SelectItem value="07">
07
</SelectItem>
<SelectItem value="08">
08
</SelectItem>
<SelectItem value="09">
09
</SelectItem>
<SelectItem value="10">
10
</SelectItem>
<SelectItem value="11">
11
</SelectItem>
<SelectItem value="12">
12
</SelectItem>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel for="checkout-7j9-exp-year-f59">
Year
</FieldLabel>
<Select default-value="">
<SelectTrigger id="checkout-7j9-exp-year-f59">
<SelectValue placeholder="YYYY" />
</SelectTrigger>
<SelectContent>
<SelectItem value="2024">
2024
</SelectItem>
<SelectItem value="2025">
2025
</SelectItem>
<SelectItem value="2026">
2026
</SelectItem>
<SelectItem value="2027">
2027
</SelectItem>
<SelectItem value="2028">
2028
</SelectItem>
<SelectItem value="2029">
2029
</SelectItem>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel for="checkout-7j9-cvv">
CVV
</FieldLabel>
<Input id="checkout-7j9-cvv" placeholder="123" required />
</Field>
</div>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldLegend>Billing Address</FieldLegend>
<FieldDescription>
The billing address associated with your payment method
</FieldDescription>
<FieldGroup>
<Field orientation="horizontal">
<Checkbox
id="checkout-7j9-same-as-shipping-wgm"
:default-value="true"
/>
<FieldLabel
for="checkout-7j9-same-as-shipping-wgm"
class="font-normal"
>
Same as shipping address
</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>
<FieldSet>
<FieldGroup>
<Field>
<FieldLabel for="checkout-7j9-optional-comments">
Comments
</FieldLabel>
<Textarea
id="checkout-7j9-optional-comments"
placeholder="Add any additional comments"
class="resize-none"
/>
</Field>
</FieldGroup>
</FieldSet>
<Field orientation="horizontal">
<Button type="submit">
Submit
</Button>
<Button variant="outline" type="button">
Cancel
</Button>
</Field>
</FieldGroup>
</form>
</div>
</template>这里是一些复选框表单域
你的桌面和文稿文件夹正在与 iCloud 云盘同步。你可以从其他设备访问它们。
<script setup lang="ts">
import { Checkbox } from '@/components/ui/checkbox'
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from '@/components/ui/field'
</script>
<template>
<div class="w-full max-w-md">
<FieldGroup>
<FieldSet>
<FieldLegend variant="label">
Show these items on the desktop
</FieldLegend>
<FieldDescription>
Select the items you want to show on the desktop.
</FieldDescription>
<FieldGroup class="gap-3">
<Field orientation="horizontal">
<Checkbox id="finder-pref-9k2-hard-disks-ljj" />
<FieldLabel
for="finder-pref-9k2-hard-disks-ljj"
class="font-normal"
:default-value="true"
>
Hard disks
</FieldLabel>
</Field>
<Field orientation="horizontal">
<Checkbox id="finder-pref-9k2-external-disks-1yg" />
<FieldLabel
for="finder-pref-9k2-external-disks-1yg"
class="font-normal"
>
External disks
</FieldLabel>
</Field>
<Field orientation="horizontal">
<Checkbox id="finder-pref-9k2-cds-dvds-fzt" />
<FieldLabel
for="finder-pref-9k2-cds-dvds-fzt"
class="font-normal"
>
CDs, DVDs, and iPods
</FieldLabel>
</Field>
<Field orientation="horizontal">
<Checkbox id="finder-pref-9k2-connected-servers-6l2" />
<FieldLabel
for="finder-pref-9k2-connected-servers-6l2"
class="font-normal"
>
Connected servers
</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<Field orientation="horizontal">
<Checkbox id="finder-pref-9k2-sync-folders-nep" :default-value="true" />
<FieldContent>
<FieldLabel for="finder-pref-9k2-sync-folders-nep">
Sync Desktop & Documents folders
</FieldLabel>
<FieldDescription>
Your Desktop & Documents folders are being synced with iCloud
Drive. You can access them from other devices.
</FieldDescription>
</FieldContent>
</Field>
</FieldGroup>
</div>
</template>你可以使用 FieldGroup 和 FieldSet 对表单域进行分组。非常适合多部分表单。
<template>
<FieldSet>
<FieldLegend />
<FieldGroup>
<Field />
<Field />
</FieldGroup>
</FieldSet>
</template><script setup lang="ts">
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from '@/components/ui/field'
import { Input } from '@/components/ui/input'
</script>
<template>
<div class="w-full max-w-md space-y-6">
<FieldSet>
<FieldLegend>Address Information</FieldLegend>
<FieldDescription>
We need your address to deliver your order.
</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel for="street">
Street Address
</FieldLabel>
<Input id="street" type="text" placeholder="123 Main St" />
</Field>
<div class="grid grid-cols-2 gap-4">
<Field>
<FieldLabel for="city">
City
</FieldLabel>
<Input id="city" type="text" placeholder="New York" />
</Field>
<Field>
<FieldLabel for="zip">
Postal Code
</FieldLabel>
<Input id="zip" type="text" placeholder="90502" />
</Field>
</div>
</FieldGroup>
</FieldSet>
</div>
</template>响应式适配非常简单。使用 orientation="responsive",它会根据容器宽度自动在垂直和水平布局之间切换。搞定。
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
</script>
<template>
<div class="w-full max-w-4xl">
<form>
<FieldSet>
<FieldLegend>Profile</FieldLegend>
<FieldDescription>Fill in your profile information.</FieldDescription>
<FieldSeparator />
<FieldGroup>
<Field orientation="responsive">
<FieldContent>
<FieldLabel for="name">
Name
</FieldLabel>
<FieldDescription>
Provide your full name for identification
</FieldDescription>
</FieldContent>
<Input id="name" placeholder="Evil Rabbit" required />
</Field>
<FieldSeparator />
<Field orientation="responsive">
<FieldContent>
<FieldLabel for="lastName">
Message
</FieldLabel>
<FieldDescription>
You can write your message here. Keep it short, preferably
under 100 characters.
</FieldDescription>
</FieldContent>
<Textarea
id="message"
placeholder="Hello, world!"
required
class="min-h-[100px] resize-none sm:min-w-[300px]"
/>
</Field>
<FieldSeparator />
<Field orientation="responsive">
<Button type="submit">
Submit
</Button>
<Button type="button" variant="outline">
Cancel
</Button>
</Field>
</FieldGroup>
</FieldSet>
</form>
</div>
</template>等等,还有更多。用 FieldLabel 包裹你的表单域,即可创建一个可选择的表单域组。非常简单,而且看起来很棒。
<script setup lang="ts">
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSet,
FieldTitle,
} from '@/components/ui/field'
import {
RadioGroup,
RadioGroupItem,
} from '@/components/ui/radio-group'
</script>
<template>
<div class="w-full max-w-md">
<FieldGroup>
<FieldSet>
<FieldLabel for="compute-environment-p8w">
Compute Environment
</FieldLabel>
<FieldDescription>
Select the compute environment for your cluster.
</FieldDescription>
<RadioGroup default-value="kubernetes">
<FieldLabel for="kubernetes-r2h">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Kubernetes</FieldTitle>
<FieldDescription>
Run GPU workloads on a K8s configured cluster.
</FieldDescription>
</FieldContent>
<RadioGroupItem id="kubernetes-r2h" value="kubernetes" />
</Field>
</FieldLabel>
<FieldLabel for="vm-z4k">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Virtual Machine</FieldTitle>
<FieldDescription>
Access a VM configured cluster to run GPU workloads.
</FieldDescription>
</FieldContent>
<RadioGroupItem id="vm-z4k" value="vm" />
</Field>
</FieldLabel>
</RadioGroup>
</FieldSet>
</FieldGroup>
</div>
</template>Item (条目)
这是一个简洁的 flex 容器,可以容纳几乎任何类型的内容。
我已经多次编写过这种逻辑,因此我决定为它创建一个组件。现在我一直在使用它,用来展示列表项、卡片等等。
这是一个基础条目
<script setup lang="ts">
import {
Item,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from '@/components/ui/item'
</script>
<template>
<Item>
<ItemMedia variant="icon">
<HomeIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Dashboard</ItemTitle>
<ItemDescription>Overview of your account and activity.</ItemDescription>
</ItemContent>
</Item>
</template>一个带有标题和描述的简单条目。
<script setup lang="ts">
import { BadgeCheckIcon, ChevronRightIcon } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from '@/components/ui/item'
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-6">
<Item variant="outline">
<ItemContent>
<ItemTitle>Basic Item</ItemTitle>
<ItemDescription>
A simple item with title and description.
</ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="outline" size="sm">
Action
</Button>
</ItemActions>
</Item>
<Item variant="outline" size="sm" as-child>
<a href="#">
<ItemMedia>
<BadgeCheckIcon class="size-5" />
</ItemMedia>
<ItemContent>
<ItemTitle>Your profile has been verified.</ItemTitle>
</ItemContent>
<ItemActions>
<ChevronRightIcon class="size-4" />
</ItemActions>
</a>
</Item>
</div>
</template>你可以为条目添加图标、头像或图片。
检测到来自未知设备的新登录。
<script setup lang="ts">
import { ShieldAlertIcon } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from '@/components/ui/item'
</script>
<template>
<div class="flex w-full max-w-lg flex-col gap-6">
<Item variant="outline">
<ItemMedia variant="icon">
<ShieldAlertIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Security Alert</ItemTitle>
<ItemDescription>
New login detected from unknown device.
</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm" variant="outline">
Review
</Button>
</ItemActions>
</Item>
</div>
</template>
ER上次登录是在 5 个月前
CN
LR
ER邀请您的团队成员共同参与该项目。
<script setup lang="ts">
import { Plus } from 'lucide-vue-next'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from '@/components/ui/item'
</script>
<template>
<div class="flex w-full max-w-lg flex-col gap-6">
<Item variant="outline">
<ItemMedia>
<Avatar class="size-10">
<AvatarImage src="https://github.com/evilrabbit.png" />
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</ItemMedia>
<ItemContent>
<ItemTitle>Evil Rabbit</ItemTitle>
<ItemDescription>Last seen 5 months ago</ItemDescription>
</ItemContent>
<ItemActions>
<Button
size="icon-sm"
variant="outline"
class="rounded-full"
aria-label="Invite"
>
<Plus />
</Button>
</ItemActions>
</Item>
<Item variant="outline">
<ItemMedia>
<div class="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
<Avatar class="hidden sm:flex">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar class="hidden sm:flex">
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</div>
</ItemMedia>
<ItemContent>
<ItemTitle>No Team Members</ItemTitle>
<ItemDescription>
Invite your team to collaborate on this project.
</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm" variant="outline">
Invite
</Button>
</ItemActions>
</Item>
</div>
</template>这是使用 ItemGroup 组成的列表效果
sshadcn@vercel.com
mmaxleiter@vercel.com
eevilrabbit@vercel.com
<script setup lang="ts">
import { Plus } from 'lucide-vue-next'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemSeparator,
ItemTitle,
} from '@/components/ui/item'
const people = [
{
username: 'shadcn',
avatar: 'https://github.com/shadcn.png',
email: 'shadcn@vercel.com',
},
{
username: 'maxleiter',
avatar: 'https://github.com/maxleiter.png',
email: 'maxleiter@vercel.com',
},
{
username: 'evilrabbit',
avatar: 'https://github.com/evilrabbit.png',
email: 'evilrabbit@vercel.com',
},
]
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-6">
<ItemGroup>
<template v-for="(person, index) in people" :key="person.username">
<Item>
<ItemMedia>
<Avatar>
<AvatarImage :src="person.avatar" class="grayscale" />
<AvatarFallback>{{ person.username.charAt(0) }}</AvatarFallback>
</Avatar>
</ItemMedia>
<ItemContent class="gap-1">
<ItemTitle>{{ person.username }}</ItemTitle>
<ItemDescription>{{ person.email }}</ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="ghost" size="icon" class="rounded-full">
<Plus />
</Button>
</ItemActions>
</Item>
<ItemSeparator v-if="index !== people.length - 1" />
</template>
</ItemGroup>
</div>
</template>需要将其作为链接?使用 asChild 属性
<template>
<Item as-child>
<a href="/dashboard">
<ItemMedia variant="icon">
<HomeIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Dashboard</ItemTitle>
<ItemDescription>Overview of your account and activity.</ItemDescription>
</ItemContent>
</a>
</Item>
</template><script setup lang="ts">
import { ChevronRightIcon, ExternalLinkIcon } from 'lucide-vue-next'
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemTitle,
} from '@/components/ui/item'
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-4">
<Item as-child>
<a href="#">
<ItemContent>
<ItemTitle>Visit our documentation</ItemTitle>
<ItemDescription>
Learn how to get started with our components.
</ItemDescription>
</ItemContent>
<ItemActions>
<ChevronRightIcon class="size-4" />
</ItemActions>
</a>
</Item>
<Item variant="outline" as-child>
<a href="#" target="_blank" rel="noopener noreferrer">
<ItemContent>
<ItemTitle>External resource</ItemTitle>
<ItemDescription>
Opens in a new tab with security attributes.
</ItemDescription>
</ItemContent>
<ItemActions>
<ExternalLinkIcon class="size-4" />
</ItemActions>
</a>
</Item>
</div>
</template>空状态
好的,最后一个:Empty (空状态)。在你的应用中展示空状态时使用它。
这是使用方法
<script setup lang="ts">
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty'
</script>
<template>
<Empty>
<EmptyMedia variant="icon">
<InboxIcon />
</EmptyMedia>
<EmptyTitle>No messages</EmptyTitle>
<EmptyDescription>You don't have any messages yet.</EmptyDescription>
<EmptyContent>
<Button>Send a message</Button>
</EmptyContent>
</Empty>
</template><script setup lang="ts">
import { ArrowUpRightIcon, FolderCode } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty'
</script>
<template>
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<FolderCode />
</EmptyMedia>
<EmptyTitle>No Projects Yet</EmptyTitle>
<EmptyDescription>
You haven't created any projects yet. Get started by creating your first
project.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div class="flex gap-2">
<Button>Create Project</Button>
<Button variant="outline">
Import Project
</Button>
</div>
</EmptyContent>
<Button variant="link" as-child class="text-muted-foreground" size="sm">
<a href="#">
Learn More <ArrowUpRightIcon />
</a>
</Button>
</Empty>
</template>你可以将其与头像一起使用
ZN该用户目前处于离线状态。你可以留言通知他们或稍后再试。
<script setup lang="ts">
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty'
</script>
<template>
<Empty>
<EmptyHeader>
<EmptyMedia variant="default">
<Avatar class="size-12">
<AvatarImage
src="https://github.com/zernonia.png"
class="grayscale"
/>
<AvatarFallback>ZN</AvatarFallback>
</Avatar>
</EmptyMedia>
<EmptyTitle>User Offline</EmptyTitle>
<EmptyDescription>
This user is currently offline. You can leave a message to notify them
or try again later.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button size="sm">
Leave Message
</Button>
</EmptyContent>
</Empty>
</template>或者将其与输入组配合使用,用于搜索结果或邮件订阅等场景
你寻找的页面不存在。尝试在下方搜索你需要的内容。
需要帮助?联系支持
<script setup lang="ts">
import { SearchIcon } from 'lucide-vue-next'
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from '@/components/ui/empty'
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from '@/components/ui/input-group'
import { Kbd } from '@/components/ui/kbd'
</script>
<template>
<Empty>
<EmptyHeader>
<EmptyTitle>404 - Not Found</EmptyTitle>
<EmptyDescription>
The page you're looking for doesn't exist. Try searching for what you
need below.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<InputGroup class="sm:w-3/4">
<InputGroupInput placeholder="Try searching for pages..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Kbd>/</Kbd>
</InputGroupAddon>
</InputGroup>
<EmptyDescription>
Need help? <a href="#">Contact support</a>
</EmptyDescription>
</EmptyContent>
</Empty>
</template>就这样。七个新组件。兼容你所有的库。随时可用于你的项目中。
2025 年 2 月 - Reka UI 和 npx shadcn-vue@latest init
我们已更新最新注册表,以支持 Reka UI 而非 Radix Vue。
更新后的 CLI 现已发布。你现在可以使用 npx shadcn-vue add 安装组件、主题、组合式函数(composables)、工具函数等。
这是向分发你和你的 LLM 都能轻松访问并使用的代码迈出的重大一步。
随着 Reka UI v2 的发布,shadcn-vue@latest 命令现在将安装 Reka UI。如果你想继续使用 Radix Vue,请访问此处,改为运行 shadcn-vue@radix 命令。
- 首先,当你初始化新应用时,我们会更新你现有的 Tailwind 文件,而不是进行覆盖。
- 现在组件会自带其依赖项。以 Accordion(手风琴)为例,它可以定义自己的 Tailwind 关键帧。当你将其添加到项目时,我们会相应地更新你的 tailwind.config.ts 文件。
- 你还可以通过 URL 安装远程组件。
npx shadcn-vue add https://acme.com/registry/navbar.json。
- 我们创建了一个新的架构,你可以使用它来发布自己的组件注册表。由于它支持 URL,你甚至可以用它来分发私有组件。
- 此外还有更多更新,例如更好的错误处理和 monorepo 支持。
你可以立即尝试使用新的 CLI。
pnpm dlx shadcn-vue@latest init Sidebar01 Login01