2025年11月 - 赶上进度
🌐 November 2025 - Catch up
我们已将 shadcn-vue.com 升级到 Nuxt v4 和 Tailwind v4。该网站现在使用升级后的 new-york 组件。
🌐 We've upgraded shadcn-vue.com to Nuxt v4 and Tailwind v4. The site now uses the upgraded new-york components.
我们还进行了一些小的设计更新,使网站更快速、更易于导航。
🌐 We've also made some minor design updates to make the site faster and easier to navigate.
图表
🌐 Chart
我们还重构了Chart组件,以匹配原始组件,并尽可能保持与API一致。
🌐 We also refactored the Chart component to match the original component, and stick to the API as close as possible.
2025年10月 - 新组件
🌐 October 2025 - New Components
对于这一轮组件,我看了我们每天构建的东西,那些我们一遍又一遍重建的乏味东西,并且制作了你实际上可以使用的可重用抽象。
🌐 For this round of components, I looked at what we build every day, the boring stuff we rebuild over and over, and made reusable abstractions you can actually use.
- Spinner:用于显示加载状态的指示器。
- Kbd:显示一个键盘按键或一组按键。
- 按钮组:一组用于操作和分割按钮的按钮。
- 输入组:包含图标、按钮、标签等的输入。
- 字段:一个组件。你所有的表单。
- 项目:显示项目列表、卡片等。
- 空:用于空状态。
旋转器
🌐 Spinner
好吧,让我们从最简单的开始:Spinner 和 Kbd。非常基础。我们都知道它们的作用。
🌐 Okay let's start with the easiest ones: Spinner and Kbd. Pretty basic. We all know what they do.
这是你呈现加载动画的方法:
🌐 Here's how you render a spinner:
<script setup lang="ts">
import { Spinner } from '@/components/ui/spinner'
</script>
<template>
<Spinner />
</template>它看起来像这样:
🌐 Here's what it looks like:
<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>它在按钮上的样子是这样的:
🌐 Here's what it looks like in a button:
<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>你可以编辑代码,并用你自己的加载动画替换它。
🌐 You can edit the code and replace it with your own 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 是一个呈现键盘按键的组件。
🌐 Kbd is a component that renders a keyboard key.
<script setup lang="ts">
import { Kbd, KbdGroup } from '@/components/ui/kbd'
</script>
<template>
<Kbd>Ctrl</Kbd>
</template>使用 KbdGroup 将键盘上的按键分组。
🌐 Use KbdGroup to group keyboard keys together.
<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>你可以将其添加到按钮、工具提示、输入组等。
🌐 You can add it to buttons, tooltips, input groups, and more.
按钮组
🌐 Button Group
我收到了很多关于这个的请求:按钮组。它是一个容器,用于将相关按钮组合在一起,并保持一致的样式。非常适合操作组、分割按钮等。
🌐 I got a lot of requests for this one: Button Group. It's a container that groups related buttons together with consistent styling. Great for action groups, split buttons, and more.
<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>这是代码:
🌐 Here's the code:
<script setup lang="ts">
import { ButtonGroup } from '@/components/ui/button-group'
</script>
<template>
<ButtonGroup>
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>
</template>你可以嵌套按钮组以创建具有间距的更复杂的布局。
🌐 You can nest button groups to create more complex layouts with spacing.
<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 创建分割按钮。经典下拉模式。
🌐 Use ButtonGroupSeparator to create split buttons. Classic dropdown pattern.
<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>你也可以使用它向输入框添加前缀或后缀按钮和文本。
🌐 You can also use it to add prefix or suffix buttons and text to inputs.
<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
输入组让你可以在输入框中添加图标、按钮等元素。你知道的,那些你总是需要在输入框周围的小东西。
🌐 Input Group lets you add icons, buttons, and more to your inputs. You know, all those little bits you always need around your inputs.
<script setup lang="ts">
import { InputGroup, InputGroupAddon, InputGroupInput } from '@/components/ui/input-group'
</script>
<template>
<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
</InputGroup>
</template>这是带图标的预览:
🌐 Here's a preview with icons:
<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>你也可以向输入组添加按钮。
🌐 You can also add buttons to the input group.
<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>或文本、标签、工具提示,...
🌐 Or text, labels, tooltips,...
<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>它也适用于文本区域,因此你可以构建具有许多旋钮和拨盘的复杂组件,或者另一个提示表单。
🌐 It also works with textareas so you can build really complex components with lots of knobs and dials or yet another prompt form.
<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>哦,这里有一些带旋转器的很酷的东西:
🌐 Oh here are some cool ones with spinners:
<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,一个用于构建非常复杂表单的组件。这种抽象非常美妙。
🌐 Introducing Field, a component for building really complex forms. The abstraction here is beautiful.
我花了很长时间才做好,但我让它与你所有的表单库一起工作:Vee Validate、TanStack Form、Bring Your Own Form。
🌐 It took me a long time to get it right but I made it work with all your form libraries: Vee Validate, TanStack Form, Bring Your Own Form.
这是一个带有输入框的基本字段:
🌐 Here's a basic field with an input:
<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>它适用于所有表单控件。输入框、文本区域、下拉选择框、复选框、单选按钮、开关、滑块,尽你所能想到的都有。下面是一个完整的示例:
🌐 It works with all form controls. Inputs, textareas, selects, checkboxes, radios, switches, sliders, you name it. Here's a full example:
<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>这里有一些复选框字段:
🌐 Here are some checkbox fields:
Your Desktop & Documents folders are being synced with iCloud Drive. You can access them from other devices.
<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 将字段分组。非常适合多部分表单。
🌐 You can group fields together using FieldGroup and FieldSet. Perfect for
multi-section forms.
<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",它会根据容器宽度在垂直和水平布局之间切换。完成。
🌐 Making it responsive is easy. Use orientation="responsive" and it switches
between vertical and horizontal layouts based on container width. Done.
<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 中以创建一个可选择的字段组。非常简单。而且看起来很棒。
🌐 Wait here's more. Wrap your fields in FieldLabel to create a selectable field group. Really easy. And it looks great.
<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
这是一个简单的弹性容器,几乎可以容纳任何类型的内容。
🌐 This one is a straightforward flex container that can house nearly any type of content.
我已经多次构建这个,因此我决定为它创建一个组件。现在我一直在使用它。我用它来显示项目列表、卡片等等。
🌐 I've built this so many times that I decided to create a component for it. Now I use it all the time. I use it to display lists of items, cards, and more.
这是一个基本条目:
🌐 Here's a basic item:
<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>A simple item with title and description.
<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>你可以向该项目添加图标、头像或图片。
🌐 You can add icons, avatars, or images to the item.
New login detected from unknown device.
<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>
ERLast seen 5 months ago
CN
LR
ERInvite your team to collaborate on this project.
<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 的项目列表的样子:
🌐 And here's what a list of items looks like with 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 属性:
🌐 Need it as a link? Use the asChild prop:
<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
好了,最后一个:空。在你的应用中使用它来显示空状态。
🌐 Okay last one: Empty. Use this to display empty states in your app.
使用方法如下:
🌐 Here's how you use it:
<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>You haven't created any projects yet. Get started by creating your first project.
<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>你可以将它用于头像:
🌐 You can use it with avatars:
ZNThis user is currently offline. You can leave a message to notify them or try again later.
<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>或者对于搜索结果或电子邮件订阅等内容,使用输入组:
🌐 Or with input groups for things like search results or email subscriptions:
The page you're looking for doesn't exist. Try searching for what you need below.
Need help? Contact support
<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>就是这样。七个新组件。兼容你所有的库。为你的项目做好准备。
🌐 That's it. Seven new components. Works with all your libraries. Ready for your projects.
2025年2月 - Reka UI & npx shadcn-vue@latest init
🌐 February 2025 - Reka UI & npx shadcn-vue@latest init
我们已更新最新注册表以支持 Reka UI,而不是 Radix Vue。
🌐 We've updated the latest registry to support Reka UI instead of Radix Vue.
更新后的命令行接口现在可用。你现在可以使用 npx shadcn-vue add 安装组件、主题、可组合函数、工具等。
🌐 The updated CLI is now available. You can now install components, themes, composables, utils and more using npx shadcn-vue add.
这是向你和你的 LLM 可以访问和使用的代码分发迈出的重要一步。
🌐 This is a major step towards distributing code that you and your LLMs can access and use.
随着 Reka UI v2 的发布,现在 shadcn-vue@latest 命令将安装 Reka UI。如果你想继续使用 Radix Vue,请访问 这里 并运行 shadcn-vue@radix 命令。::
- 首先,当你初始化一个新应用时,我们会更新你现有的 Tailwind 文件,而不是覆盖它们。
- 一个组件现在可以自带它自己的依赖。例如手风琴组件,它可以定义自己的 Tailwind 动画关键帧。当你将它添加到项目中时,我们会相应地更新你的 tailwind.config.ts 文件。
- 你也可以使用 URL 安装远程组件。
npx shadcn-vue add https://acme.com/registry/navbar.json。
- 我们创建了一个新的模式,你可以使用它来发布你自己的组件注册表。由于它支持 URL,你甚至可以用它来分发私有组件。
- 还有一些更新,例如更好的错误处理和 monorepo 支持。
你今天可以尝试新的 cli。
🌐 You can try the new cli today.
pnpm dlx shadcn-vue@latest init Sidebar01 Login01