Skip to content

Calendar

Alpha
Displays dates and days of the week, facilitating date-related interactions.
December 2024
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
Event Date, December 2024
vue
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNext, CalendarPrev, CalendarRoot, type CalendarRootProps } from 'radix-vue'

const isDateUnavailable: CalendarRootProps['isDateUnavailable'] = (date) => {
  return date.day === 17 || date.day === 18
}
</script>

<template>
  <CalendarRoot
    v-slot="{ weekDays, grid }"
    :is-date-unavailable="isDateUnavailable"
    class="mt-6 rounded-xl bg-white p-4 shadow-md"
    fixed-weeks
  >
    <CalendarHeader class="flex items-center justify-between">
      <CalendarPrev
        class="inline-flex items-center cursor-pointer text-black justify-center rounded-[9px] bg-transparent w-8 h-8 hover:bg-black hover:text-white active:scale-98 active:transition-all focus:shadow-[0_0_0_2px] focus:shadow-black"
      >
        <Icon
          icon="radix-icons:chevron-left"
          class="w-6 h-6"
        />
      </CalendarPrev>
      <CalendarHeading class="text-[15px] text-black font-medium" />

      <CalendarNext
        class="inline-flex items-center cursor-pointer justify-center text-black rounded-[9px] bg-transparent w-8 h-8 hover:bg-black hover:text-white active:scale-98 active:transition-all focus:shadow-[0_0_0_2px] focus:shadow-black"
      >
        <Icon
          icon="radix-icons:chevron-right"
          class="w-6 h-6"
        />
      </CalendarNext>
    </CalendarHeader>
    <div
      class="flex flex-col space-y-4 pt-4 sm:flex-row sm:space-x-4 sm:space-y-0"
    >
      <CalendarGrid
        v-for="month in grid"
        :key="month.value.toString()"
        class="w-full border-collapse select-none space-y-1"
      >
        <CalendarGridHead>
          <CalendarGridRow class="mb-1 grid w-full grid-cols-7">
            <CalendarHeadCell
              v-for="day in weekDays"
              :key="day"
              class="rounded-md text-xs text-green8"
            >
              {{ day }}
            </CalendarHeadCell>
          </CalendarGridRow>
        </CalendarGridHead>
        <CalendarGridBody class="grid">
          <CalendarGridRow
            v-for="(weekDates, index) in month.rows"
            :key="`weekDate-${index}`"
            class="grid grid-cols-7"
          >
            <CalendarCell
              v-for="weekDate in weekDates"
              :key="weekDate.toString()"
              :date="weekDate"
              class="relative text-center text-sm"
            >
              <CalendarCellTrigger
                :day="weekDate"
                :month="month.value"
                class="relative flex items-center justify-center rounded-full whitespace-nowrap text-sm font-normal text-black w-8 h-8 outline-none focus:shadow-[0_0_0_2px] focus:shadow-black data-[disabled]:text-black/30 data-[selected]:!bg-green10 data-[selected]:text-white hover:bg-green5 data-[highlighted]:bg-green5 data-[unavailable]:pointer-events-none data-[unavailable]:text-black/30 data-[unavailable]:line-through before:absolute before:top-[5px] before:hidden before:rounded-full before:w-1 before:h-1 before:bg-white data-[today]:before:block data-[today]:before:bg-green9 "
              />
            </CalendarCell>
          </CalendarGridRow>
        </CalendarGridBody>
      </CalendarGrid>
    </div>
  </CalendarRoot>
</template>

Credit

This component was built taking inspiration from the implementation in melt-ui.

Features

  • Full keyboard navigation
  • Can be controlled or uncontrolled
  • Focus is fully managed
  • Localization support
  • Highly composable

Preface

The component depends on the @internationalized/date package, which solves a lot of the problems that come with working with dates and times in JavaScript.

We highly recommend reading through the documentation for the package to get a solid feel for how it works, and you'll need to install it in your project to use the date-related components.

Installation

Install the date package.

sh
$ npm add @internationalized/date

Install the component from your command line.

sh
$ npm add radix-vue

Anatomy

Import all parts and piece them together.

vue
<script setup>
import {
  CalendarCell,
  CalendarCellTrigger,
  CalendarGrid,
  CalendarGridBody,
  CalendarGridHead,
  CalendarGridRow,
  CalendarHeadCell,
  CalendarHeader,
  CalendarHeading,

  CalendarNext,
  CalendarPrev,
  CalendarRoot
} from 'radix-vue'
</script>

<template>
  <CalendarRoot>
    <CalendarHeader>
      <CalendarPrev />
      <CalendarHeading />
      <CalendarNext />
    </CalendarHeader>
    <CalendarGrid>
      <CalendarGridHead>
        <CalendarGridRow>
          <CalendarHeadCell />
        </CalendarGridRow>
      </CalendarGridHead>
      <CalendarGridBody>
        <CalendarGridRow>
          <CalendarCell>
            <CalendarCellTrigger />
          </CalendarCell>
        </CalendarGridRow>
      </CalendarGridBody>
    </CalendarGrid>
  </CalendarRoot>
</template>

API Reference

Root

Contains all the parts of a calendar

PropDefaultType
as
'div'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

calendarLabel
string

The accessible label for the calendar

defaultPlaceholder
DateValue

The default placeholder date

defaultValue
DateValue

The default value for the calendar

dir
'ltr' | 'rtl'

The reading direction of the calendar when applicable.
If omitted, inherits globally from ConfigProvider or assumes LTR (left-to-right) reading mode.

disabled
false
boolean

Whether or not the calendar is disabled

fixedWeeks
false
boolean

Whether or not to always display 6 weeks in the calendar

initialFocus
false
boolean

If true, the calendar will focus the selected day, today, or the first day of the month depending on what is visible when the calendar is mounted

isDateDisabled
Matcher

A function that returns whether or not a date is disabled

isDateUnavailable
Matcher

A function that returns whether or not a date is unavailable

locale
'en'
string

The locale to use for formatting dates

maxValue
DateValue

The maximum date that can be selected

minValue
DateValue

The minimum date that can be selected

modelValue
DateValue | DateValue[]

The controlled checked state of the calendar. Can be bound as v-model.

multiple
false
boolean

Whether or not multiple dates can be selected

nextPage
((placeholder: DateValue) => DateValue)

A function that returns the next page of the calendar. It receives the current placeholder as an argument inside the component.

numberOfMonths
1
number

The number of months to display at once

pagedNavigation
false
boolean

This property causes the previous and next buttons to navigate by the number of months displayed at once, rather than one month

placeholder
DateValue

The placeholder date, which is used to determine what month to display when no date is selected. This updates as the user navigates the calendar and can be used to programmatically control the calendar view

preventDeselect
false
boolean

Whether or not to prevent the user from deselecting a date without selecting another date first

prevPage
((placeholder: DateValue) => DateValue)

A function that returns the previous page of the calendar. It receives the current placeholder as an argument inside the component.

readonly
false
boolean

Whether or not the calendar is readonly

weekdayFormat
'narrow'
'narrow' | 'short' | 'long'

The format to use for the weekday strings provided via the weekdays slot prop

weekStartsOn
0
0 | 1 | 2 | 3 | 4 | 5 | 6

The day of the week to start the calendar on

EmitPayload
update:modelValue
[date: DateValue]

Event handler called whenever the model value changes

update:placeholder
[date: DateValue]

Event handler called whenever the placeholder value changes

Slots (default)Payload
date
DateValue

The current date of the placeholder

grid
Grid<DateValue>[]

The grid of dates

weekDays
string[]

The days of the week

weekStartsOn
0 | 1 | 2 | 3 | 4 | 5 | 6

The start of the week

locale
string

The calendar locale

fixedWeeks
boolean

Whether or not to always display 6 weeks in the calendar

Data AttributeValue
[data-readonly]Present when readonly
[data-disabled]Present when disabled
[data-invalid]Present when invalid

Contains the navigation buttons and the heading segments.

PropDefaultType
as
'div'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

Prev Button

Calendar navigation button. It navigates the calendar one month/year/decade in the past based on the current calendar view.

PropDefaultType
as
'button'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

prevPage
((placeholder: DateValue) => DateValue)

The function to be used for the prev page. Overwrites the prevPage function set on the CalendarRoot.

step
'month'
'month' | 'year'

The calendar unit to go back

Data AttributeValue
[data-disabled]Present when disabled

Next Button

Calendar navigation button. It navigates the calendar one month/year/decade in the future based on the current calendar view.

PropDefaultType
as
'button'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

nextPage
((placeholder: DateValue) => DateValue)

The function to be used for the next page. Overwrites the nextPage function set on the CalendarRoot.

step
'month'
'month' | 'year'

The calendar unit to go forward

Data AttributeValue
[data-disabled]Present when disabled

Heading

Heading for displaying the current month and year

PropDefaultType
as
'div'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

Slots (default)Payload
headingValue
string

Current month and year

Data AttributeValue
[data-disabled]Present when disabled

Grid

Container for wrapping the calendar grid.

PropDefaultType
as
'table'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

Data AttributeValue
[data-readonly]Present when readonly
[data-disabled]Present when disabled

Grid Head

Container for wrapping the grid head.

PropDefaultType
as
'thead'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

Grid Body

Container for wrapping the grid body.

PropDefaultType
as
'tbody'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

Grid Row

Container for wrapping the grid row.

PropDefaultType
as
'tr'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

Head Cell

Container for wrapping the head cell. Used for displaying the week days.

PropDefaultType
as
'th'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

Cell

Container for wrapping the calendar cells.

PropDefaultType
as
'td'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

date*
DateValue

The date value for the cell

Data AttributeValue
[data-disabled]Present when disabled

Cell Trigger

Interactable container for displaying the cell dates. Clicking it selects the date.

PropDefaultType
as
'div'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

day*
DateValue

The date value provided to the cell trigger

month*
DateValue

The month in which the cell is rendered

Slots (default)Payload
dayValue
string

Current day

Data AttributeValue
[data-selected]Present when selected
[data-value]The ISO string value of the date.
[data-disabled]Present when disabled
[data-unavailable]Present when unavailable
[data-today]Present when today
[data-outside-view]Present when the date is outside the current month it is displayed in.
[data-outside-visible-view]Present when the date is outside the months that are visible on the calendar.
[data-focused]Present when focused

Examples

Calendar with Year Incrementation

This example showcases a calendar which allows incrementing the year.

December 2024
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
Event Date, December 2024
vue
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import type { DateValue } from '@internationalized/date'
import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNext, CalendarPrev, CalendarRoot, type CalendarRootProps } from 'radix-vue'

const isDateUnavailable: CalendarRootProps['isDateUnavailable'] = (date) => {
  return date.day === 17 || date.day === 18
}

function pagingFunc(date: DateValue, sign: -1 | 1) {
  if (sign === -1)
    return date.subtract({ years: 1 })
  return date.add({ years: 1 })
}
</script>

<template>
  <CalendarRoot
    v-slot="{ weekDays, grid }"
    :is-date-unavailable="isDateUnavailable"
    class="mt-6 rounded-xl bg-white p-4 shadow-md"
    fixed-weeks
  >
    <CalendarHeader class="flex items-center justify-between">
      <CalendarPrev
        class="inline-flex items-center cursor-pointer text-black justify-center rounded-[9px] bg-transparent w-8 h-8 hover:bg-black hover:text-white active:scale-98 active:transition-all focus:shadow-[0_0_0_2px] focus:shadow-black"
        :prev-page="(date: DateValue) => pagingFunc(date, -1)"
      >
        <Icon
          icon="radix-icons:double-arrow-left"
          class="w-6 h-6"
        />
      </CalendarPrev>
      <CalendarPrev
        class="inline-flex items-center cursor-pointer text-black justify-center rounded-[9px] bg-transparent w-8 h-8 hover:bg-black hover:text-white active:scale-98 active:transition-all focus:shadow-[0_0_0_2px] focus:shadow-black"
      >
        <Icon
          icon="radix-icons:chevron-left"
          class="w-6 h-6"
        />
      </CalendarPrev>
      <CalendarHeading class="text-[15px] text-black font-medium" />

      <CalendarNext
        class="inline-flex items-center cursor-pointer justify-center text-black rounded-[9px] bg-transparent w-8 h-8 hover:bg-black hover:text-white active:scale-98 active:transition-all focus:shadow-[0_0_0_2px] focus:shadow-black"
      >
        <Icon
          icon="radix-icons:chevron-right"
          class="w-6 h-6"
        />
      </CalendarNext>

      <CalendarNext
        class="inline-flex items-center cursor-pointer justify-center text-black rounded-[9px] bg-transparent w-8 h-8 hover:bg-black hover:text-white active:scale-98 active:transition-all focus:shadow-[0_0_0_2px] focus:shadow-black"
        :next-page="(date: DateValue) => pagingFunc(date, 1)"
      >
        <Icon
          icon="radix-icons:double-arrow-right"
          class="w-6 h-6"
        />
      </CalendarNext>
    </CalendarHeader>
    <div
      class="flex flex-col space-y-4 pt-4 sm:flex-row sm:space-x-4 sm:space-y-0"
    >
      <CalendarGrid
        v-for="month in grid"
        :key="month.value.toString()"
        class="w-full border-collapse select-none space-y-1"
      >
        <CalendarGridHead>
          <CalendarGridRow class="mb-1 grid w-full grid-cols-7">
            <CalendarHeadCell
              v-for="day in weekDays"
              :key="day"
              class="rounded-md text-xs text-green8"
            >
              {{ day }}
            </CalendarHeadCell>
          </CalendarGridRow>
        </CalendarGridHead>
        <CalendarGridBody class="grid">
          <CalendarGridRow
            v-for="(weekDates, index) in month.rows"
            :key="`weekDate-${index}`"
            class="grid grid-cols-7"
          >
            <CalendarCell
              v-for="weekDate in weekDates"
              :key="weekDate.toString()"
              :date="weekDate"
              class="relative text-center text-sm"
            >
              <CalendarCellTrigger
                :day="weekDate"
                :month="month.value"
                class="relative flex items-center justify-center rounded-full whitespace-nowrap text-sm font-normal text-black w-8 h-8 outline-none focus:shadow-[0_0_0_2px] focus:shadow-black data-[disabled]:text-black/30 data-[selected]:!bg-green10 data-[selected]:text-white hover:bg-green5 data-[highlighted]:bg-green5 data-[unavailable]:pointer-events-none data-[unavailable]:text-black/30 data-[unavailable]:line-through before:absolute before:top-[5px] before:hidden before:rounded-full before:w-1 before:h-1 before:bg-white data-[today]:before:block data-[today]:before:bg-green9 "
              />
            </CalendarCell>
          </CalendarGridRow>
        </CalendarGridBody>
      </CalendarGrid>
    </div>
  </CalendarRoot>
</template>

Calendar with Locale and Calendar System Selection

This example showcases some of the available locales and how the calendar systems are displayed.

December 2024
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
Event Date, December 2024
vue
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { createCalendar, getLocalTimeZone, toCalendar, today } from '@internationalized/date'
import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNext, CalendarPrev, CalendarRoot, Label, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectPortal, SelectRoot, SelectScrollUpButton, SelectTrigger, SelectValue, SelectViewport } from 'radix-vue'
import { computed, ref } from 'vue'

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)

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: string) {
  locale.value = newLocale
  calendar.value = pref.value!.ordering.split(' ')[0]
}
const value = computed(() => toCalendar(today(getLocalTimeZone()), createCalendar(calendar.value)))
</script>

<template>
  <div class="flex flex-col gap-4">
    <Label class="text-white">Locale</Label>
    <SelectRoot
      v-model="locale"
      @update:model-value="updateLocale"
    >
      <SelectTrigger
        class="inline-flex min-w-[160px] items-center justify-between rounded px-[15px] text-[13px] leading-none h-[35px] gap-[5px] bg-white text-grass11 shadow-[0_2px_10px] shadow-black/10 hover:bg-mauve3 focus:shadow-[0_0_0_2px] focus:shadow-black data-[placeholder]:text-green9 outline-none"
        aria-label="Select a locale"
      >
        <SelectValue placeholder="Please select a locale">
          {{ pref!.label }}
        </SelectValue>
        <Icon
          icon="radix-icons:chevron-down"
          class="h-3.5 w-3.5"
        />
      </SelectTrigger>

      <SelectPortal>
        <SelectContent
          class="min-w-[160px] bg-white rounded shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),_0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)] will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade z-[100]"
          :side-offset="5"
        >
          <SelectScrollUpButton class="flex items-center justify-center h-[25px] bg-white text-violet11 cursor-default">
            <Icon icon="radix-icons:chevron-up" />
          </SelectScrollUpButton>

          <SelectViewport class="p-[5px]">
            <SelectItem
              v-for="(option, index) in preferences"
              :key="index"
              class="text-[13px] 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"
            >
              <SelectItemIndicator class="absolute left-0 w-[25px] inline-flex items-center justify-center">
                <Icon icon="radix-icons:check" />
              </SelectItemIndicator>
              <SelectItemText>
                {{ option.label }}
              </SelectItemText>
            </SelectItem>
          </SelectViewport>

          <SelectScrollDownButton class="flex items-center justify-center h-[25px] bg-white text-violet11 cursor-default">
            <Icon icon="radix-icons:chevron-down" />
          </SelectScrollDownButton>
        </SelectContent>
      </SelectPortal>
    </SelectRoot>
    <Label class="text-white">Calendar</Label>
    <SelectRoot v-model="calendar">
      <SelectTrigger
        class="inline-flex min-w-[160px] items-center justify-between rounded px-[15px] text-[13px] leading-none h-[35px] gap-[5px] bg-white text-grass11 shadow-[0_2px_10px] shadow-black/10 hover:bg-mauve3 focus:shadow-[0_0_0_2px] focus:shadow-black data-[placeholder]:text-green9 outline-none"
        aria-label="Select a calendar"
      >
        <SelectValue placeholder="Please select a calendar">
          {{ calendars.find(c => c.key === calendar)?.name }}
        </SelectValue>

        <Icon
          icon="radix-icons:chevron-down"
          class="h-3.5 w-3.5"
        />
      </SelectTrigger>

      <SelectPortal>
        <SelectContent
          class="min-w-[160px] bg-white rounded shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),_0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)] will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade z-[100]"
          :side-offset="5"
        >
          <SelectScrollUpButton class="flex items-center justify-center h-[25px] bg-white text-violet11 cursor-default">
            <Icon icon="radix-icons:chevron-up" />
          </SelectScrollUpButton>

          <SelectViewport class="p-[5px]">
            <SelectLabel class="px-[25px] text-xs leading-[25px] text-mauve11">
              Preferred
            </SelectLabel>
            <SelectGroup>
              <SelectItem
                v-for="(option, index) in preferredCalendars"
                :key="index"
                class="text-[13px] 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"
              >
                <SelectItemIndicator class="absolute left-0 w-[25px] inline-flex items-center justify-center">
                  <Icon icon="radix-icons:check" />
                </SelectItemIndicator>
                <SelectItemText>
                  {{ option!.name }}
                </SelectItemText>
              </SelectItem>
            </SelectGroup>
            <SelectSeparator class="h-[1px] bg-green6 m-[5px]" />
            <SelectLabel class="px-[25px] text-xs leading-[25px] text-mauve11">
              Other
            </SelectLabel>
            <SelectGroup>
              <SelectItem
                v-for="(option, index) in otherCalendars"
                :key="index"
                class="text-[13px] 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"
              >
                <SelectItemIndicator class="absolute left-0 w-[25px] inline-flex items-center justify-center">
                  <Icon icon="radix-icons:check" />
                </SelectItemIndicator>
                <SelectItemText>
                  {{ option.name }}
                </SelectItemText>
              </SelectItem>
            </SelectGroup>
          </SelectViewport>

          <SelectScrollDownButton class="flex items-center justify-center h-[25px] bg-white text-violet11 cursor-default">
            <Icon icon="radix-icons:chevron-down" />
          </SelectScrollDownButton>
        </SelectContent>
      </SelectPortal>
    </SelectRoot>

    <CalendarRoot
      v-slot="{ weekDays, grid }"
      :model-value="value"
      :locale="locale"
      class="mt-6 rounded-xl bg-white p-4 shadow-md"
      fixed-weeks
    >
      <CalendarHeader class="flex items-center justify-between">
        <CalendarPrev
          class="inline-flex items-center cursor-pointer text-black justify-center rounded-[9px] bg-transparent w-8 h-8 hover:bg-black hover:text-white active:scale-98 active:transition-all focus:shadow-[0_0_0_2px] focus:shadow-black"
        >
          <Icon
            icon="radix-icons:chevron-left"
            class="w-6 h-6"
          />
        </CalendarPrev>
        <CalendarHeading class="text-[15px] text-black font-medium" />

        <CalendarNext
          class="inline-flex items-center cursor-pointer justify-center text-black rounded-[9px] bg-transparent w-8 h-8 hover:bg-black hover:text-white active:scale-98 active:transition-all focus:shadow-[0_0_0_2px] focus:shadow-black"
        >
          <Icon
            icon="radix-icons:chevron-right"
            class="w-6 h-6"
          />
        </CalendarNext>
      </CalendarHeader>
      <div
        class="flex flex-col space-y-4 pt-4 sm:flex-row sm:space-x-4 sm:space-y-0"
      >
        <CalendarGrid
          v-for="month in grid"
          :key="month.value.toString()"
          class="w-full border-collapse select-none space-y-1"
        >
          <CalendarGridHead>
            <CalendarGridRow class="mb-1 grid w-full grid-cols-7">
              <CalendarHeadCell
                v-for="day in weekDays"
                :key="day"
                class="rounded-md text-xs text-green8"
              >
                {{ day }}
              </CalendarHeadCell>
            </CalendarGridRow>
          </CalendarGridHead>
          <CalendarGridBody class="grid">
            <CalendarGridRow
              v-for="(weekDates, index) in month.rows"
              :key="`weekDate-${index}`"
              class="grid grid-cols-7"
            >
              <CalendarCell
                v-for="weekDate in weekDates"
                :key="weekDate.toString()"
                :date="weekDate"
                class="relative text-center text-sm"
              >
                <CalendarCellTrigger
                  :day="weekDate"
                  :month="month.value"
                  class="relative flex items-center justify-center rounded-full whitespace-nowrap text-sm font-normal text-black w-8 h-8 outline-none focus:shadow-[0_0_0_2px] focus:shadow-black data-[disabled]:text-black/30 data-[selected]:!bg-green10 data-[selected]:text-white hover:bg-green5 data-[highlighted]:bg-green5 data-[unavailable]:pointer-events-none data-[unavailable]:text-black/30 data-[unavailable]:line-through before:absolute before:top-[5px] before:hidden before:rounded-full before:w-1 before:h-1 before:bg-white data-[today]:before:block data-[today]:before:bg-green9 "
                />
              </CalendarCell>
            </CalendarGridRow>
          </CalendarGridBody>
        </CalendarGrid>
      </div>
    </CalendarRoot>
  </div>
</template>

Accessibility

Keyboard Interactions

KeyDescription
Tab
When focus moves onto the calendar, focuses the first navigation button.
Space
When the focus is on either CalendarNext or CalendarPrev, it navigates the calendar. Otherwise, it selects the date.
Enter
When the focus is on either CalendarNext or CalendarPrev, it navigates the calendar. Otherwise, it selects the date.
ArrowLeftArrowRightArrowUpArrowDown
When the focus is on CalendarCellTrigger, it navigates the dates, changing the month/year/decade if necessary.