A calendar component for selecting single dates, multiple dates or date ranges.
This component relies on the @internationalized/date package which provides objects and functions for representing and manipulating dates and times in a locale-aware manner.

Usage

Use the v-model directive to control the selected date.

February 2022
30
31
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
1
2
3
4
5
6
7
8
9
10
11
12
Event Date, February 2022
<script setup lang="ts">
const value = ref(new CalendarDate(2022, 2, 3))
</script>

<template>
  <UCalendar v-model="value" />
</template>

Use the default-value prop to set the initial value when you do not need to control its state.

February 2022
30
31
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
1
2
3
4
5
6
7
8
9
10
11
12
Event Date, February 2022
<script setup lang="ts">
const defaultValue = ref(new CalendarDate(2022, 2, 6))
</script>

<template>
  <UCalendar :default-value="defaultValue" />
</template>

Multiple

Use the multiple prop to allow multiple selections.

February 2022
30
31
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
1
2
3
4
5
6
7
8
9
10
11
12
Event Date, February 2022
<script setup lang="ts">
const value = ref([
  new CalendarDate(2022, 2, 4),
  new CalendarDate(2022, 2, 6),
  new CalendarDate(2022, 2, 8)
])
</script>

<template>
  <UCalendar multiple v-model="value" />
</template>

Range

Use the range prop to select a range of dates.

Event Date, February 2022
February 2022
30
31
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
1
2
3
4
5
6
7
8
9
10
11
12
<script setup lang="ts">
const value = ref({ start: new CalendarDate(2022, 2, 3), end: new CalendarDate(2022, 2, 20) })
</script>

<template>
  <UCalendar range v-model="value" />
</template>

Color

Use the color prop to change the color of the calendar.

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
<template>
  <UCalendar color="neutral" />
</template>

Size

Use the size prop to change the size of the calendar.

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
<template>
  <UCalendar size="xl" />
</template>

Disabled

Use the disabled prop to disable the calendar.

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
<template>
  <UCalendar disabled />
</template>

Number Of Months

Use the numberOfMonths prop to change the number of months in the calendar.

December 2024 - February 2025
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
29
30
31
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
26
27
28
29
30
31
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
1
2
3
4
5
6
7
8
Event Date, December 2024 - February 2025
<template>
  <UCalendar :number-of-months="3" />
</template>

Month Controls

Use the month-controls prop to show the month controls. Defaults to true.

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
<template>
  <UCalendar :monthControls="false" />
</template>

Year Controls

Use the year-controls prop to show the year controls. Defaults to true.

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
<template>
  <UCalendar :yearControls="false" />
</template>

Fixed Weeks

Use the fixed-weeks prop to display the calendar with fixed weeks.

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
Event Date, December 2024
<template>
  <UCalendar :fixedWeeks="false" />
</template>

Examples

With chip events

Use the Chip component to add events to specific days.

January 2022
26
27
28
29
30
31
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
Event Date, January 2022
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef(new CalendarDate(2022, 1, 10))

function getColorByDate(date: Date) {
  const isWeekend = date.getDay() % 6 == 0
  const isDayMeeting = date.getDay() % 3 == 0

  if (isWeekend) {
    return undefined
  }

  if (isDayMeeting) {
    return 'error'
  }

  return 'success'
}
</script>

<template>
  <UCalendar v-model="modelValue">
    <template #day="{ day }">
      <UChip :show="!!getColorByDate(day.toDate('UTC'))" :color="getColorByDate(day.toDate('UTC'))" size="2xs">
        {{ day.day }}
      </UChip>
    </template>
  </UCalendar>
</template>

With disabled dates

Use the is-date-disabled prop with a function to mark specific dates as disabled.

Event Date, January 2022
January 2022
26
27
28
29
30
31
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
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'
import type { Matcher } from 'reka-ui/date'

const modelValue = shallowRef({
  start: new CalendarDate(2022, 1, 1),
  end: new CalendarDate(2022, 1, 9)
})

const isDateDisabled: Matcher = (date) => {
  return date.day >= 10 && date.day <= 16
}
</script>

<template>
  <UCalendar v-model="modelValue" :is-date-disabled="isDateDisabled" range />
</template>

With unavailable dates

Use the is-date-unavailable prop with a function to mark specific dates as unavailable.

Event Date, January 2022
January 2022
26
27
28
29
30
31
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
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'
import type { Matcher } from 'reka-ui/date'

const modelValue = shallowRef({
  start: new CalendarDate(2022, 1, 1),
  end: new CalendarDate(2022, 1, 9)
})

const isDateUnavailable: Matcher = (date) => {
  return date.day >= 10 && date.day <= 16
}
</script>

<template>
  <UCalendar v-model="modelValue" :is-date-unavailable="isDateUnavailable" range />
</template>

With min/max dates

Use the min-value and max-value props to limit the dates.

September 2023
27
28
29
30
31
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
1
2
3
4
5
6
7
Event Date, September 2023
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef(new CalendarDate(2023, 9, 10))
const minDate = new CalendarDate(2023, 9, 1)
const maxDate = new CalendarDate(2023, 9, 30)
</script>

<template>
  <UCalendar v-model="modelValue" :min-value="minDate" :max-value="maxDate" />
</template>

As a DatePicker

Use a Button and a Popover component to create a date picker.

<script setup lang="ts">
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'

const df = new DateFormatter('en-US', {
  dateStyle: 'medium'
})

const modelValue = shallowRef(new CalendarDate(2022, 1, 10))
</script>

<template>
  <UPopover>
    <UButton color="neutral" variant="subtle" icon="i-lucide-calendar">
      {{ df.format(modelValue.toDate(getLocalTimeZone())) }}
    </UButton>

    <template #content>
      <UCalendar v-model="modelValue" class="p-2" />
    </template>
  </UPopover>
</template>

As a DateRangePicker

Use a Button and a Popover component to create a date range picker.

<script setup lang="ts">
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'

const df = new DateFormatter('en-US', {
  dateStyle: 'medium'
})

const modelValue = shallowRef({
  start: new CalendarDate(2022, 1, 20),
  end: new CalendarDate(2022, 2, 10)
})
</script>

<template>
  <UPopover>
    <UButton color="neutral" variant="subtle" icon="i-lucide-calendar">
      <template v-if="modelValue.start">
        <template v-if="modelValue.end">
          {{ df.format(modelValue.start.toDate(getLocalTimeZone())) }} - {{ df.format(modelValue.end.toDate(getLocalTimeZone())) }}
        </template>

        <template v-else>
          {{ df.format(modelValue.start.toDate(getLocalTimeZone())) }}
        </template>
      </template>
      <template v-else>
        Pick a date
      </template>
    </UButton>

    <template #content>
      <UCalendar v-model="modelValue" class="p-2" :number-of-months="2" range />
    </template>
  </UPopover>
</template>

API

Props

Prop Default Type
as

'div'

any

The element or component this component should render as.

nextYearIcon

appConfig.ui.icons.chevronDoubleRight

string

The icon to use for the next year control.

nextMonthIcon

appConfig.ui.icons.chevronRight

string

The icon to use for the next month control.

prevYearIcon

appConfig.ui.icons.chevronDoubleLeft

string

The icon to use for the previous year control.

prevMonthIcon

appConfig.ui.icons.chevronLeft

string

The icon to use for the previous month control.

color

'primary'

"error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"

size

'md'

"md" | "xs" | "sm" | "lg" | "xl"

range

boolean

Whether or not a range of dates can be selected

multiple

boolean

Whether or not multiple dates can be selected

monthControls

true

boolean

Show month controls

yearControls

true

boolean

Show year controls

defaultValue

CalendarDate | CalendarDateTime | ZonedDateTime | DateRange | DateValue[]

modelValue

CalendarDate | CalendarDateTime | ZonedDateTime | DateRange | DateValue[]

disabled

boolean

Whether or not the calendar is disabled

defaultPlaceholder

CalendarDate | CalendarDateTime | ZonedDateTime

The default placeholder date

placeholder

CalendarDate | CalendarDateTime | ZonedDateTime

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

pagedNavigation

boolean

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

preventDeselect

boolean

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

weekStartsOn

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

The day of the week to start the calendar on

weekdayFormat

"narrow" | "short" | "long"

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

fixedWeeks

true

boolean

Whether or not to always display 6 weeks in the calendar

maxValue

CalendarDate | CalendarDateTime | ZonedDateTime

The maximum date that can be selected

minValue

CalendarDate | CalendarDateTime | ZonedDateTime

The minimum date that can be selected

numberOfMonths

number

The number of months to display at once

readonly

boolean

Whether or not the calendar is readonly

initialFocus

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

(date: DateValue): boolean

A function that returns whether or not a date is disabled

isDateUnavailable

(date: DateValue): boolean

A function that returns whether or not a date is unavailable

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.

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.

ui

Partial<{ root: string; header: string; body: string; heading: string; grid: string; gridRow: string; gridWeekDaysRow: string; gridBody: string; headCell: string; cell: string; cellTrigger: string[]; }>

Slots

Slot Type
heading

{ value: string; }

day

Pick<CalendarCellTriggerProps, "day">

week-day

{ day: string; }

Emits

Event Type
update:modelValue

[date: DateValue | DateRange | DateValue[]]

update:placeholder

[date: DateValue] & [date: DateValue]

update:startValue

[date: DateValue | undefined]

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    calendar: {
      slots: {
        root: '',
        header: 'flex items-center justify-between',
        body: 'flex flex-col space-y-4 pt-4 sm:flex-row sm:space-x-4 sm:space-y-0',
        heading: 'text-center font-medium truncate mx-auto',
        grid: 'w-full border-collapse select-none space-y-1 focus:outline-none',
        gridRow: 'grid grid-cols-7',
        gridWeekDaysRow: 'mb-1 grid w-full grid-cols-7',
        gridBody: 'grid',
        headCell: 'rounded-[calc(var(--ui-radius)*1.5)]',
        cell: 'relative text-center',
        cellTrigger: [
          'm-0.5 relative flex items-center justify-center rounded-full whitespace-nowrap focus-visible:ring-2 focus:outline-none data-disabled:text-[var(--ui-text-muted)] data-unavailable:line-through data-unavailable:text-[var(--ui-text-muted)] data-unavailable:pointer-events-none data-[selected]:text-[var(--ui-bg)] data-today:font-semibold',
          'transition'
        ]
      },
      variants: {
        color: {
          primary: {
            headCell: 'text-[var(--ui-primary)]',
            cellTrigger: 'focus-visible:ring-[var(--ui-primary)] data-[selected]:bg-[var(--ui-primary)] data-today:not-data-[selected]:text-[var(--ui-primary)] data-[highlighted]:bg-[var(--ui-primary)]/20 hover:not-data-[selected]:bg-[var(--ui-primary)]/20'
          },
          secondary: {
            headCell: 'text-[var(--ui-secondary)]',
            cellTrigger: 'focus-visible:ring-[var(--ui-secondary)] data-[selected]:bg-[var(--ui-secondary)] data-today:not-data-[selected]:text-[var(--ui-secondary)] data-[highlighted]:bg-[var(--ui-secondary)]/20 hover:not-data-[selected]:bg-[var(--ui-secondary)]/20'
          },
          success: {
            headCell: 'text-[var(--ui-success)]',
            cellTrigger: 'focus-visible:ring-[var(--ui-success)] data-[selected]:bg-[var(--ui-success)] data-today:not-data-[selected]:text-[var(--ui-success)] data-[highlighted]:bg-[var(--ui-success)]/20 hover:not-data-[selected]:bg-[var(--ui-success)]/20'
          },
          info: {
            headCell: 'text-[var(--ui-info)]',
            cellTrigger: 'focus-visible:ring-[var(--ui-info)] data-[selected]:bg-[var(--ui-info)] data-today:not-data-[selected]:text-[var(--ui-info)] data-[highlighted]:bg-[var(--ui-info)]/20 hover:not-data-[selected]:bg-[var(--ui-info)]/20'
          },
          warning: {
            headCell: 'text-[var(--ui-warning)]',
            cellTrigger: 'focus-visible:ring-[var(--ui-warning)] data-[selected]:bg-[var(--ui-warning)] data-today:not-data-[selected]:text-[var(--ui-warning)] data-[highlighted]:bg-[var(--ui-warning)]/20 hover:not-data-[selected]:bg-[var(--ui-warning)]/20'
          },
          error: {
            headCell: 'text-[var(--ui-error)]',
            cellTrigger: 'focus-visible:ring-[var(--ui-error)] data-[selected]:bg-[var(--ui-error)] data-today:not-data-[selected]:text-[var(--ui-error)] data-[highlighted]:bg-[var(--ui-error)]/20 hover:not-data-[selected]:bg-[var(--ui-error)]/20'
          },
          neutral: {
            headCell: 'text-[var(--ui-bg-inverted)]',
            cellTrigger: 'focus-visible:ring-[var(--ui-border-inverted)] data-[selected]:bg-[var(--ui-bg-inverted)] data-today:not-data-[selected]:text-[var(--ui-bg-inverted)] data-[highlighted]:bg-[var(--ui-bg-inverted)]/20 hover:not-data-[selected]:bg-[var(--ui-bg-inverted)]/10'
          }
        },
        size: {
          xs: {
            heading: 'text-xs',
            cell: 'text-xs',
            headCell: 'text-[10px]',
            cellTrigger: 'size-7',
            body: 'space-y-2 pt-2'
          },
          sm: {
            heading: 'text-xs',
            headCell: 'text-xs',
            cell: 'text-xs',
            cellTrigger: 'size-7'
          },
          md: {
            heading: 'text-sm',
            headCell: 'text-xs',
            cell: 'text-sm',
            cellTrigger: 'size-8'
          },
          lg: {
            heading: 'text-md',
            headCell: 'text-md',
            cellTrigger: 'size-9 text-md'
          },
          xl: {
            heading: 'text-lg',
            headCell: 'text-lg',
            cellTrigger: 'size-10 text-lg'
          }
        }
      },
      defaultVariants: {
        size: 'md',
        color: 'primary'
      }
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        calendar: {
          slots: {
            root: '',
            header: 'flex items-center justify-between',
            body: 'flex flex-col space-y-4 pt-4 sm:flex-row sm:space-x-4 sm:space-y-0',
            heading: 'text-center font-medium truncate mx-auto',
            grid: 'w-full border-collapse select-none space-y-1 focus:outline-none',
            gridRow: 'grid grid-cols-7',
            gridWeekDaysRow: 'mb-1 grid w-full grid-cols-7',
            gridBody: 'grid',
            headCell: 'rounded-[calc(var(--ui-radius)*1.5)]',
            cell: 'relative text-center',
            cellTrigger: [
              'm-0.5 relative flex items-center justify-center rounded-full whitespace-nowrap focus-visible:ring-2 focus:outline-none data-disabled:text-[var(--ui-text-muted)] data-unavailable:line-through data-unavailable:text-[var(--ui-text-muted)] data-unavailable:pointer-events-none data-[selected]:text-[var(--ui-bg)] data-today:font-semibold',
              'transition'
            ]
          },
          variants: {
            color: {
              primary: {
                headCell: 'text-[var(--ui-primary)]',
                cellTrigger: 'focus-visible:ring-[var(--ui-primary)] data-[selected]:bg-[var(--ui-primary)] data-today:not-data-[selected]:text-[var(--ui-primary)] data-[highlighted]:bg-[var(--ui-primary)]/20 hover:not-data-[selected]:bg-[var(--ui-primary)]/20'
              },
              secondary: {
                headCell: 'text-[var(--ui-secondary)]',
                cellTrigger: 'focus-visible:ring-[var(--ui-secondary)] data-[selected]:bg-[var(--ui-secondary)] data-today:not-data-[selected]:text-[var(--ui-secondary)] data-[highlighted]:bg-[var(--ui-secondary)]/20 hover:not-data-[selected]:bg-[var(--ui-secondary)]/20'
              },
              success: {
                headCell: 'text-[var(--ui-success)]',
                cellTrigger: 'focus-visible:ring-[var(--ui-success)] data-[selected]:bg-[var(--ui-success)] data-today:not-data-[selected]:text-[var(--ui-success)] data-[highlighted]:bg-[var(--ui-success)]/20 hover:not-data-[selected]:bg-[var(--ui-success)]/20'
              },
              info: {
                headCell: 'text-[var(--ui-info)]',
                cellTrigger: 'focus-visible:ring-[var(--ui-info)] data-[selected]:bg-[var(--ui-info)] data-today:not-data-[selected]:text-[var(--ui-info)] data-[highlighted]:bg-[var(--ui-info)]/20 hover:not-data-[selected]:bg-[var(--ui-info)]/20'
              },
              warning: {
                headCell: 'text-[var(--ui-warning)]',
                cellTrigger: 'focus-visible:ring-[var(--ui-warning)] data-[selected]:bg-[var(--ui-warning)] data-today:not-data-[selected]:text-[var(--ui-warning)] data-[highlighted]:bg-[var(--ui-warning)]/20 hover:not-data-[selected]:bg-[var(--ui-warning)]/20'
              },
              error: {
                headCell: 'text-[var(--ui-error)]',
                cellTrigger: 'focus-visible:ring-[var(--ui-error)] data-[selected]:bg-[var(--ui-error)] data-today:not-data-[selected]:text-[var(--ui-error)] data-[highlighted]:bg-[var(--ui-error)]/20 hover:not-data-[selected]:bg-[var(--ui-error)]/20'
              },
              neutral: {
                headCell: 'text-[var(--ui-bg-inverted)]',
                cellTrigger: 'focus-visible:ring-[var(--ui-border-inverted)] data-[selected]:bg-[var(--ui-bg-inverted)] data-today:not-data-[selected]:text-[var(--ui-bg-inverted)] data-[highlighted]:bg-[var(--ui-bg-inverted)]/20 hover:not-data-[selected]:bg-[var(--ui-bg-inverted)]/10'
              }
            },
            size: {
              xs: {
                heading: 'text-xs',
                cell: 'text-xs',
                headCell: 'text-[10px]',
                cellTrigger: 'size-7',
                body: 'space-y-2 pt-2'
              },
              sm: {
                heading: 'text-xs',
                headCell: 'text-xs',
                cell: 'text-xs',
                cellTrigger: 'size-7'
              },
              md: {
                heading: 'text-sm',
                headCell: 'text-xs',
                cell: 'text-sm',
                cellTrigger: 'size-8'
              },
              lg: {
                heading: 'text-md',
                headCell: 'text-md',
                cellTrigger: 'size-9 text-md'
              },
              xl: {
                heading: 'text-lg',
                headCell: 'text-lg',
                cellTrigger: 'size-10 text-lg'
              }
            }
          },
          defaultVariants: {
            size: 'md',
            color: 'primary'
          }
        }
      }
    })
  ]
})