- 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
- 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
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
<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>About
The <Calendar /> component is built on top of the Reka UI Calendar component, which uses the @internationalized/date package to handle dates.
If you're looking for a range calendar, check out the Range Calendar component.
Installation
pnpm dlx shadcn-vue@latest add calendar
Usage
<script setup lang="ts">
import { Calendar } from '@/components/ui/calendar'
</script>
<template>
<Calendar />
</template>Calendar Systems (Persian / Hijri / Jalali for example)
@internationalized/date Supports 13 calendar systems
Here, we'll use the Persian calendar as an example to show how to use calendar systems with the <Calendar /> component or any other Calendar components.
The default calendar system is gregory.
To use a different calendar system, you need to provide a value with the desired system through the defaultPlaceholder or placeholder props.
It's recommended to add either the placeholder or defaultPlaceholder to the component even if you don't use any other calendar system
<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>If none of these props are provided, the emitted dates will use the gregorian calendar by default, since it is the most widely used system.
The emitted value from the Calendar component will vary depending on the specified calendar system identifier.
You can also change the locale using the locale prop to match the calendar system interface.
<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>| ش | ی | د | س | چ | پ | ج |
|---|---|---|---|---|---|---|
<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>Examples
Calendar Systems
importing createCalendar into your project will result in all available calendars being included in your bundle. If you wish to limit the supported calendars to reduce bundle sizes, you can create your own implementation that only imports the desired classes. This way, your bundler can tree-shake the unused calendar implementations.
Check @internationalized/date, especially the section on Calendar Identifiers.
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}`)
}
}| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
<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>Month and Year Selector
Make sure to pass either the placeholder or defaultPlaceholder prop when using this feature.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
<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>Date of Birth Picker
<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>Date and Time Picker
<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>Natural Language Picker
This component uses the chrono-node library to parse natural language dates.
<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>Custom Heading and Cell Size
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
<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>