组件
- 手风琴 (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)
开始使用
4月
2026
| 日 | 一 | 二 | 三 | 二 | 五 | 日 |
|---|---|---|---|---|---|---|
活动日期,2026年4月
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { CalendarDate, fromDate, getLocalTimeZone } from '@internationalized/date'
import { Calendar } from '@/components/ui/calendar'
const date = ref(fromDate(new Date(), getLocalTimeZone())) as Ref<DateValue>
</script>
<template>
<Calendar
v-model="date"
class="rounded-md border shadow-sm"
layout="month-and-year"
:min-value="new CalendarDate(1925, 1, 1)"
:max-value="new CalendarDate(2035, 1, 1)"
/>
</template>关于
<Calendar /> 组件构建在 Reka UI Calendar 组件之上,该组件使用 @internationalized/date 包来处理日期。
如果您正在寻找范围日历,请查看 Range Calendar 组件。
安装
pnpm dlx shadcn-vue@latest add calendar
使用方法
<script setup lang="ts">
import { Calendar } from '@/components/ui/calendar'
</script>
<template>
<Calendar />
</template>日历系统(例如:波斯历 / 回历 / 贾拉里历)
@internationalized/date 支持 13 种日历系统。在此,我们将以波斯历为例,展示如何将日历系统与 <Calendar /> 组件或任何其他日历组件一起使用。
默认的日历系统是 gregory(格里高利历)。
若要使用其他日历系统,您需要通过 defaultPlaceholder 或 placeholder 属性提供带有所需系统的值。
即使您不使用其他日历系统,也建议在组件中添加 placeholder 或 defaultPlaceholder。
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { getLocalTimeZone, PersianCalendar, toCalendar, today } from '@internationalized/date'
import { Calendar } from '@/registry/new-york-v4/ui/calendar'
const date = ref(today(getLocalTimeZone())) as Ref<DateValue> // no need to add calendar identifier to modelValue when using placeholder
const placeholder = ref(toCalendar(today(getLocalTimeZone()), new PersianCalendar())) as Ref<DateValue>
// or
const defaultPlaceholder = toCalendar(today(getLocalTimeZone()))
</script>
<template>
<Calendar
v-model="date"
v-model:placeholder="placeholder"
locale="fa-IR"
/>
<!-- or -->
<Calendar
v-model="date"
:default-placeholder="placeholder"
locale="fa-IR"
/>
</template>如果未提供这些属性,则触发的日期默认将使用 gregorian(格里高利历)日历,因为它是最广泛使用的系统。
从日历组件触发的值将根据指定的日历系统标识符而有所不同。
您还可以使用 locale 属性更改区域设置,以匹配日历系统界面。
<script setup lang="ts">
import {
CalendarDate,
fromDate,
getLocalTimeZone,
parseDate,
PersianCalendar,
toCalendar,
today
} from '@internationalized/date'
import { ref } from 'vue'
const date = ref(toCalendar(new CalendarDate(2025, 1, 1), new PersianCalendar()))
// or
const date = ref(toCalendar(parseDate('2022-02-03'), new PersianCalendar()))
// or
const date = ref(toCalendar(today(getLocalTimeZone()), new PersianCalendar()))
// or
const date = ref(new CalendarDate(new PersianCalendar(), 1404, 1, 1))
// or
const date = ref(toCalendar(fromDate(new Date(), getLocalTimeZone()), new PersianCalendar()))
const placeholder = ref(toCalendar(today(getLocalTimeZone()), new PersianCalendar()))
</script>
<template>
<Calendar
v-model="date"
v-model:placeholder="placeholder"
locale="fa-IR"
dir="rtl"
/>
</template>Farvardin
۱۴۰۵
| ش | ی | د | س | چ | پ | ج |
|---|---|---|---|---|---|---|
活动日期,1405年Farvardin
1405/1/22
Farvardin 1405
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { getLocalTimeZone, PersianCalendar, toCalendar, today } from '@internationalized/date'
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
import { useDateFormatter } from 'reka-ui'
import { toDate } from 'reka-ui/date'
import { Calendar } from '@/components/ui/calendar'
const date = ref(today(getLocalTimeZone())) as Ref<DateValue>
const placeholder = ref(toCalendar(today(getLocalTimeZone()), new PersianCalendar())) as Ref<DateValue>
// or
const defaultPlaceholder = toCalendar(today(getLocalTimeZone()), new PersianCalendar())
const formatter = useDateFormatter('fa')
</script>
<template>
<div class="**:data-[slot=native-select-icon]:right-[unset] **:data-[slot=native-select-icon]:left-3.5">
<Calendar
v-model="date"
v-model:placeholder="placeholder"
locale="fa-IR"
layout="month-and-year"
class="rounded-md border shadow-sm"
dir="rtl"
>
<template #calendar-next-icon>
<ChevronLeft />
</template>
<template #calendar-prev-icon>
<ChevronRight />
</template>
</Calendar>
<div class="flex flex-col justify-center items-center gap-2">
<div>
{{
formatter.custom(
toDate(date, getLocalTimeZone()), {
numberingSystem: 'latn',
})
}}
</div>
<div>
{{ formatter.custom(date.toDate(getLocalTimeZone()), { month: 'short', year: 'numeric' }) }}
</div>
</div>
</div>
</template>示例
日历系统
将 createCalendar 导入到您的项目中,将会把所有可用的日历都包含在您的 bundle 中。如果您希望限制支持的日历以减小 bundle 大小,您可以创建自己的实现,只导入所需的类。这样,您的打包器就可以通过 tree-shaking 移除未使用的日历实现。
请查阅 @internationalized/date,特别是关于 日历标识符 的部分。
import { GregorianCalendar, JapaneseCalendar } from '@internationalized/date'
function createCalendar(identifier) {
switch (identifier) {
case 'gregory':
return new GregorianCalendar()
case 'japanese':
return new JapaneseCalendar()
default:
throw new Error(`Unsupported calendar ${identifier}`)
}
}2026年4月
| 日 | 一 | 二 | 三 | 二 | 五 | 日 |
|---|---|---|---|---|---|---|
活动日期,2026年4月
<script setup lang="ts">
import type { CalendarIdentifier, DateValue } from '@internationalized/date'
import { createCalendar, getLocalTimeZone, toCalendar, today } from '@internationalized/date'
import { Calendar } from '@/components/ui/calendar'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
const date = ref(today(getLocalTimeZone())) as Ref<DateValue>
const preferences = [
{ locale: 'en-US', label: 'Default', ordering: 'gregory' },
{ label: 'Arabic (Algeria)', locale: 'ar-DZ', territories: 'DJ DZ EH ER IQ JO KM LB LY MA MR OM PS SD SY TD TN YE', ordering: 'gregory islamic islamic-civil islamic-tbla' },
{ label: 'Arabic (United Arab Emirates)', locale: 'ar-AE', territories: 'AE BH KW QA', ordering: 'gregory islamic-umalqura islamic islamic-civil islamic-tbla' },
{ label: 'Arabic (Egypt)', locale: 'AR-EG', territories: 'EG', ordering: 'gregory coptic islamic islamic-civil islamic-tbla' },
{ label: 'Arabic (Saudi Arabia)', locale: 'ar-SA', territories: 'SA', ordering: 'islamic-umalqura gregory islamic islamic-rgsa' },
{ label: 'Farsi (Iran)', locale: 'fa-IR', territories: 'IR', ordering: 'persian gregory islamic islamic-civil islamic-tbla' },
{ label: 'Farsi (Afghanistan)', locale: 'fa-AF', territories: 'AF IR', ordering: 'persian gregory islamic islamic-civil islamic-tbla' },
{ label: 'Amharic (Ethiopia)', locale: 'am-ET', territories: 'ET', ordering: 'gregory ethiopic ethioaa' },
{ label: 'Hebrew (Israel)', locale: 'he-IL', territories: 'IL', ordering: 'gregory hebrew islamic islamic-civil islamic-tbla' },
{ label: 'Hindi (India)', locale: 'hi-IN', territories: 'IN', ordering: 'gregory indian' },
{ label: 'Japanese (Japan)', locale: 'ja-JP', territories: 'JP', ordering: 'gregory japanese' },
{ label: 'Thai (Thailand)', locale: 'th-TH', territories: 'TH', ordering: 'buddhist gregory' },
{ label: 'Chinese (Taiwan)', locale: 'zh-TW', territories: 'TW', ordering: 'gregory roc chinese' },
]
const calendars = [
{ key: 'gregory', name: 'Gregorian' },
{ key: 'japanese', name: 'Japanese' },
{ key: 'buddhist', name: 'Buddhist' },
{ key: 'roc', name: 'Taiwan' },
{ key: 'persian', name: 'Persian' },
{ key: 'indian', name: 'Indian' },
{ key: 'islamic-umalqura', name: 'Islamic (Umm al-Qura)' },
{ key: 'islamic-civil', name: 'Islamic Civil' },
{ key: 'islamic-tbla', name: 'Islamic Tabular' },
{ key: 'hebrew', name: 'Hebrew' },
{ key: 'coptic', name: 'Coptic' },
{ key: 'ethiopic', name: 'Ethiopic' },
{ key: 'ethioaa', name: 'Ethiopic (Amete Alem)' },
]
const locale = ref(preferences[0]?.locale)
const calendar = ref(calendars[0]?.key) as Ref<CalendarIdentifier>
const pref = computed(() => preferences.find(p => p.locale === locale.value))
const preferredCalendars = computed(() => pref.value ? pref.value.ordering.split(' ').map(p => calendars.find(c => c.key === p)).filter(Boolean) : [calendars[0]])
const otherCalendars = computed(() => calendars.filter(c => !preferredCalendars.value.some(p => p!.key === c.key)))
function updateLocale(newLocale: any) {
locale.value = newLocale
calendar.value = pref.value!.ordering.split(' ')[0] as any
}
const placeholder = computed(() => toCalendar(today(getLocalTimeZone()), createCalendar(calendar.value)))
</script>
<template>
<div class="flex flex-col gap-4">
<Label>Locale</Label>
<Select
:model-value="locale"
@update:model-value="updateLocale"
>
<SelectTrigger class="w-full">
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="(option, index) in preferences"
:key="index"
class="text-xs leading-none text-grass11 rounded-[3px] flex items-center h-[25px] pr-[35px] pl-[25px] relative select-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-green9 data-[highlighted]:text-green1"
:value="option.locale"
>
{{ option.label }}
</SelectItem>
</SelectContent>
</Select>
<Label>Calendar</Label>
<Select v-model="calendar" class="w-full">
<SelectTrigger class="w-full">
<SelectValue placeholder="Select a calendar">
{{ calendars.find(c => c.key === calendar)?.name }}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectLabel />
<SelectGroup>
<SelectItem
v-for="(option, index) in preferredCalendars"
:key="index"
class="text-xs leading-none text-grass11 rounded-[3px] flex items-center h-[25px] pr-[35px] pl-[25px] relative select-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-green9 data-[highlighted]:text-green1"
:value="option!.key"
>
{{ option!.name }}
</SelectItem>
</SelectGroup>
<SelectSeparator />
<SelectLabel>Other</SelectLabel>
<SelectGroup>
<SelectItem
v-for="(option, index) in otherCalendars"
:key="index"
class="text-xs leading-none text-grass11 rounded-[3px] flex items-center h-[25px] pr-[35px] pl-[25px] relative select-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-green9 data-[highlighted]:text-green1"
:value="option.key"
>
{{ option.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Calendar
v-model="date"
v-model:placeholder="placeholder"
:locale="locale"
class="rounded-md border shadow-sm"
/>
</div>
</template>月份和年份选择器
使用此功能时,请确保传递 placeholder 或 defaultPlaceholder 属性。
4月
2026
| 日 | 一 | 二 | 三 | 二 | 五 | 日 |
|---|---|---|---|---|---|---|
活动日期,2026年4月
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import type { LayoutTypes } from '@/components/ui/calendar'
import { getLocalTimeZone, today } from '@internationalized/date'
import { ref } from 'vue'
import { Calendar } from '@/components/ui/calendar'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
const defaultPlaceholder = today(getLocalTimeZone())
const date = ref(today(getLocalTimeZone())) as Ref<DateValue>
const layout = ref<LayoutTypes>('month-and-year')
</script>
<template>
<div class="flex flex-col gap-4">
<Calendar
v-model="date"
:default-placeholder="defaultPlaceholder"
class="rounded-md border shadow-sm"
:layout
disable-days-outside-current-view
/>
<div class="flex flex-col gap-3">
<Label for="dropdown" class="px-1">
Dropdown
</Label>
<Select
v-model="layout"
>
<SelectTrigger
id="dropdown"
size="sm"
class="bg-background w-full"
>
<SelectValue placeholder="Dropdown" />
</SelectTrigger>
<SelectContent align="center">
<SelectItem value="month-and-year">
Month and Year
</SelectItem>
<SelectItem value="month-only">
Month Only
</SelectItem>
<SelectItem value="year-only">
Year Only
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</template>出生日期选择器
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { getLocalTimeZone, today } from '@internationalized/date'
import { ChevronDownIcon } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar'
import { Label } from '@/components/ui/label'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
const date = ref(today(getLocalTimeZone())) as Ref<DateValue>
</script>
<template>
<div class="flex flex-col gap-3">
<Label for="date" class="px-1">
Date of birth
</Label>
<Popover v-slot="{ close }">
<PopoverTrigger as-child>
<Button
id="date"
variant="outline"
class="w-48 justify-between font-normal"
>
{{ date ? date.toDate(getLocalTimeZone()).toLocaleDateString() : "Select date" }}
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto overflow-hidden p-0" align="start">
<Calendar
:model-value="date"
layout="month-and-year"
@update:model-value="(value) => {
if (value) {
date = value
close()
}
}"
/>
</PopoverContent>
</Popover>
</div>
</template>日期和时间选择器
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { getLocalTimeZone, today } from '@internationalized/date'
import { ChevronDownIcon } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
const date = ref(today(getLocalTimeZone())) as Ref<DateValue>
const open = ref(false)
</script>
<template>
<div class="flex gap-4">
<div class="flex flex-col gap-3">
<Label for="date-picker" class="px-1">
Date
</Label>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button
id="date-picker"
variant="outline"
class="w-32 justify-between font-normal"
>
{{ date ? date.toDate(getLocalTimeZone()).toLocaleDateString() : "Select date" }}
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto overflow-hidden p-0" align="start">
<Calendar
:model-value="date"
@update:model-value="(value) => {
if (value) {
date = value
open = false
}
}"
/>
</PopoverContent>
</Popover>
</div>
<div class="flex flex-col gap-3">
<Label for="time-picker" class="px-1">
Time
</Label>
<Input
id="time-picker"
type="time"
step="1"
default-value="10:30:00"
class="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
/>
</div>
</div>
</template>自然语言选择器
此组件使用 chrono-node 库来解析自然语言日期。
您的文章将于 2026年4月13日 发布。
<script lang="ts">
export function formatDate(date: Date | undefined) {
if (!date) {
return ''
}
return date.toLocaleDateString('en-US', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
}
</script>
<script setup lang="ts">
import { fromDate, getLocalTimeZone } from '@internationalized/date'
import { parseDate } from 'chrono-node'
import { CalendarIcon } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
const inputValue = ref('In 2 days')
const nativeDate = computed(() => {
return parseDate(inputValue.value)
})
const open = ref(false)
</script>
<template>
<div class="flex flex-col gap-3">
<Label for="date" class="px-1">
Schedule Date
</Label>
<div class="relative flex gap-2">
<Input
id="date"
:model-value="inputValue"
placeholder="Tomorrow or next week"
class="bg-background pr-10"
@update:model-value="(value) => {
if (value) {
inputValue = value.toString()
nativeDate = parseDate(value.toString())
}
}"
/>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button
id="date-picker"
variant="ghost"
class="absolute top-1/2 right-2 size-6 -translate-y-1/2"
>
<CalendarIcon class="size-3.5" />
<span class="sr-only">Select date</span>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto overflow-hidden p-0" align="end">
<Calendar
:model-value="fromDate(nativeDate!, getLocalTimeZone())"
@update:model-value="(value) => {
if (value) {
nativeDate = value.toDate(getLocalTimeZone())
inputValue = formatDate(value.toDate(getLocalTimeZone()))
open = false
}
}"
/>
</PopoverContent>
</Popover>
</div>
<div class="text-muted-foreground px-1 text-sm">
Your post will be published on
<span class="font-medium">{{ formatDate(nativeDate!) }}</span>.
</div>
</div>
</template>自定义标题和单元格大小
自定义标题
4月
| 日 | 一 | 二 | 三 | 四 | 五 | 六 |
|---|---|---|---|---|---|---|
活动日期,2026年4月
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { getLocalTimeZone, today } from '@internationalized/date'
import { Calendar } from '@/components/ui/calendar'
const date = ref(today(getLocalTimeZone())) as Ref<DateValue>
const defaultPlaceholder = today(getLocalTimeZone())
</script>
<template>
<Calendar
v-model="date"
:default-placeholder="defaultPlaceholder"
weekday-format="short"
class="rounded-md border shadow-sm **:data-[slot=calendar-cell-trigger]:size-12!"
>
<template #calendar-heading="{ date, month }">
<div class="flex gap-2 items-center">
<div>
Custom heading
</div>
<component :is="month" :date="date" />
</div>
</template>
</Calendar>
</template>