8.8k

Calendar

PreviousNext

A date field component that allows users to enter and edit date.

Nov
2025
Event Date, November 2025
<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>
آذر
۱۴۰۴
Event Date, ۱۴۰۴ آذر
1404/9/9
آذر ۱۴۰۴
<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}`)
  }
}
November 2025
Event Date, November 2025
<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.

Nov
2025
Event Date, November 2025
<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.

Your post will be published on December 02, 2025.
<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

Custom heading
Nov
Event Date, November 2025
<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>