Calendar

A calendar component for selecting single dates, multiple dates or date ranges.

Usage

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

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">
import { CalendarDate } from '@internationalized/date'

const value = shallowRef(new CalendarDate(2022, 2, 3))
</script>

<template>
  <UCalendar v-model="value" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

const value = shallowRef(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.

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">
import { CalendarDate } from '@internationalized/date'

const defaultValue = shallowRef(new CalendarDate(2022, 2, 6))
</script>

<template>
  <UCalendar :default-value="defaultValue" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

const defaultValue = shallowRef(new CalendarDate(2022, 2, 6))
</script>

<template>
  <UCalendar :default-value="defaultValue" />
</template>
This component uses the @internationalized/date package for locale-aware formatting. The date format is determined by the locale prop of the App component.
This component uses the @internationalized/date package for locale-aware formatting. The date format is determined by the locale prop of the App component.

Type 4.9+

Use the type prop to change what the calendar selects. Defaults to date.

When using date, click the heading to switch from the day view to a month then year view for quick navigation, then drill back down to pick a date.

Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
Month Picker, 2022
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const value = shallowRef(new CalendarDate(2022, 2, 1))
</script>

<template>
  <UCalendar type="month" v-model="value" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

const value = shallowRef(new CalendarDate(2022, 2, 1))
</script>

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

Use type="year" to render a standalone year picker.

2020 - 2031
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
Year Picker, 2020 - 2031
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

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

<template>
  <UCalendar type="year" v-model="value" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

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

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

Multiple

Use the multiple prop to allow multiple selections.

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">
import { CalendarDate } from '@internationalized/date'

const value = shallowRef([
  new CalendarDate(2022, 2, 4),
  new CalendarDate(2022, 2, 6),
  new CalendarDate(2022, 2, 8)
])
</script>

<template>
  <UCalendar multiple v-model="value" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

const value = shallowRef([
  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
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">
import { CalendarDate } from '@internationalized/date'

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

<template>
  <UCalendar range v-model="value" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

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

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

The range prop also works with type="month" and type="year", letting you select a range of months or years.

Month Picker, 2022
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

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

<template>
  <UCalendar type="month" range v-model="value" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

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

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

Number Of Months

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

28
29
30
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
29
30
31
1
2
3
4
5
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
8
9
10
Event Date, July - September 2026
<template>
  <UCalendar :number-of-months="3" />
</template>

Month Controls

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

28
29
30
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
Event Date, July 2026
<template>
  <UCalendar :month-controls="false" />
</template>

Use the prev-month and next-month props to override the month buttons.

28
29
30
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
Event Date, July 2026
<template>
  <UCalendar
    :prev-month="{
      color: 'primary',
      variant: 'soft'
    }"
    :next-month="{
      color: 'primary',
      variant: 'soft'
    }"
  />
</template>

Year Controls

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

28
29
30
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
Event Date, July 2026
<template>
  <UCalendar :year-controls="false" />
</template>

Use the prev-year and next-year props to override the year buttons.

28
29
30
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
Event Date, July 2026
<template>
  <UCalendar
    :prev-year="{
      color: 'primary',
      variant: 'soft'
    }"
    :next-year="{
      color: 'primary',
      variant: 'soft'
    }"
  />
</template>

View Control 4.9+

Use the view-control prop to make the heading a button that switches between the day, month and year views. Defaults to true.

July 2026
28
29
30
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
Event Date, July 2026
<template>
  <UCalendar :view-control="false" />
</template>

Set the view-control prop to an object to override the heading button.

28
29
30
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
Event Date, July 2026
<template>
  <UCalendar
    :view-control="{
      color: 'primary',
      variant: 'soft'
    }"
  />
</template>

Fixed Weeks

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

28
29
30
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
Event Date, July 2026
<template>
  <UCalendar :fixed-weeks="false" />
</template>

Week Numbers 4.4+

Use the week-numbers prop to display week numbers in the calendar.

27
28
29
30
1
2
3
4
28
5
6
7
8
9
10
11
29
12
13
14
15
16
17
18
30
19
20
21
22
23
24
25
31
26
27
28
29
30
31
1
32
2
3
4
5
6
7
8
Event Date, July 2026
<template>
  <UCalendar week-numbers />
</template>

Color

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

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

Variant

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

Event 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
<template>
  <UCalendar variant="subtle" />
</template>

Size

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

28
29
30
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
Event Date, July 2026
<template>
  <UCalendar size="xl" />
</template>

Disabled

Use the disabled prop to disable the calendar.

28
29
30
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
Event Date, July 2026
<template>
  <UCalendar disabled />
</template>

Examples

With chip events

Use the Chip component to add events to specific days.

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>
<script setup lang="ts">
import { shallowRef } from 'vue'
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. When using type="month" or type="year", use the is-month-disabled or is-year-disabled prop instead.

Event Date, 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 type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'

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

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

<template>
  <UCalendar v-model="modelValue" :is-date-disabled="isDateDisabled" range />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'

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

const isDateDisabled = (date: DateValue) => {
  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. When using type="month" or type="year", use the is-month-unavailable or is-year-unavailable prop instead.

Event Date, 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 type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'

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

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

<template>
  <UCalendar v-model="modelValue" :is-date-unavailable="isDateUnavailable" range />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'

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

const isDateUnavailable = (date: DateValue) => {
  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.

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>
<script setup lang="ts">
import { shallowRef } from 'vue'
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>

With other calendar systems

You can use other calenders from @internationalized/date to implement a different calendar system.

24
25
26
27
28
29
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
Event Date, Tishri 5781
<script lang="ts" setup>
import { CalendarDate, HebrewCalendar } from '@internationalized/date'

const hebrewDate = shallowRef(new CalendarDate(new HebrewCalendar(), 5781, 1, 1))
</script>

<template>
  <UCalendar v-model="hebrewDate" />
</template>
You can check all the available calendars on @internationalized/date docs.

With external controls

You can control the calendar with external controls by manipulating the date passed in the v-model.

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
8
9
10
Event Date, April 2025
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const date = shallowRef(new CalendarDate(2025, 4, 2))
</script>

<template>
  <div class="flex flex-col gap-4">
    <UCalendar v-model="date" :month-controls="false" :year-controls="false" />

    <div class="flex justify-between gap-4">
      <UButton color="neutral" variant="outline" @click="date = date.subtract({ months: 1 })">
        Prev
      </UButton>

      <UButton color="neutral" variant="outline" @click="date = date.add({ months: 1 })">
        Next
      </UButton>
    </div>
  </div>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

const date = shallowRef(new CalendarDate(2025, 4, 2))
</script>

<template>
  <div class="flex flex-col gap-4">
    <UCalendar v-model="date" :month-controls="false" :year-controls="false" />

    <div class="flex justify-between gap-4">
      <UButton color="neutral" variant="outline" @click="date = date.subtract({ months: 1 })">
        Prev
      </UButton>

      <UButton color="neutral" variant="outline" @click="date = date.add({ months: 1 })">
        Next
      </UButton>
    </div>
  </div>
</template>

With today's date

Use the today function from @internationalized/date with getLocalTimeZone to set the value to the current date.

28
29
30
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
Event Date, July 2026
<script setup lang="ts">
import { getLocalTimeZone, today } from '@internationalized/date'

const date = shallowRef(today(getLocalTimeZone()))
</script>

<template>
  <div class="flex flex-col gap-4">
    <UCalendar v-model="date" />

    <UButton color="neutral" variant="outline" class="justify-center" @click="date = today(getLocalTimeZone())">
      Today
    </UButton>
  </div>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { getLocalTimeZone, today } from '@internationalized/date'

const date = shallowRef(today(getLocalTimeZone()))
</script>

<template>
  <div class="flex flex-col gap-4">
    <UCalendar v-model="date" />

    <UButton color="neutral" variant="outline" class="justify-center" @click="date = today(getLocalTimeZone())">
      Today
    </UButton>
  </div>
</template>

As a date picker

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">
      {{ modelValue ? df.format(modelValue.toDate(getLocalTimeZone())) : 'Select a date' }}
    </UButton>

    <template #content>
      <UCalendar v-model="modelValue" class="p-2" />
    </template>
  </UPopover>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
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">
      {{ modelValue ? df.format(modelValue.toDate(getLocalTimeZone())) : 'Select a date' }}
    </UButton>

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

As a date range picker

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

<script setup lang="ts">
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import { DateFormatter, getLocalTimeZone, today } from '@internationalized/date'

const df = new DateFormatter('en-US', { dateStyle: 'medium' })
const tz = getLocalTimeZone()
const breakpoints = useBreakpoints(breakpointsTailwind)
const isDesktop = breakpoints.greaterOrEqual('sm')

const ranges = [
  { label: 'Last 7 days', days: 7 },
  { label: 'Last 14 days', days: 14 },
  { label: 'Last 30 days', days: 30 },
  { label: 'Last 3 months', months: 3 },
  { label: 'Last 6 months', months: 6 },
  { label: 'Last year', years: 1 }
]

const initialEnd = today(tz)
const modelValue = shallowRef({
  start: initialEnd.subtract({ days: 14 }),
  end: initialEnd
})

const label = computed(() => {
  const { start, end } = modelValue.value
  if (!start) return 'Pick a date'
  if (!end) return df.format(start.toDate(tz))
  return `${df.format(start.toDate(tz))} - ${df.format(end.toDate(tz))}`
})

function computeStart(range: typeof ranges[number]) {
  const end = today(tz)
  return { start: end.subtract({ days: range.days, months: range.months, years: range.years }), end }
}

function isRangeSelected(range: typeof ranges[number]) {
  if (!modelValue.value?.start || !modelValue.value?.end) return false
  const { start, end } = computeStart(range)
  return modelValue.value.start.compare(start) === 0 && modelValue.value.end.compare(end) === 0
}

function selectRange(range: typeof ranges[number]) {
  modelValue.value = computeStart(range)
}
</script>

<template>
  <UPopover :content="{ align: 'center' }">
    <UButton color="neutral" variant="subtle" icon="i-lucide-calendar">
      {{ label }}
    </UButton>

    <template #content>
      <div class="flex items-stretch divide-x divide-(--ui-border)">
        <div class="hidden sm:flex flex-col justify-center py-2">
          <UButton
            v-for="(range, index) in ranges"
            :key="index"
            :label="range.label"
            color="neutral"
            variant="ghost"
            class="rounded-none px-4"
            :class="[isRangeSelected(range) ? 'bg-elevated' : 'hover:bg-elevated/50']"
            truncate
            @click="selectRange(range)"
          />
        </div>

        <UCalendar v-model="modelValue" class="p-2" :number-of-months="isDesktop ? 2 : 1" range />
      </div>
    </template>
  </UPopover>
</template>
<script setup lang="ts">
import { shallowRef, computed } from 'vue'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import { DateFormatter, getLocalTimeZone, today } from '@internationalized/date'

const df = new DateFormatter('en-US', { dateStyle: 'medium' })
const tz = getLocalTimeZone()
const breakpoints = useBreakpoints(breakpointsTailwind)
const isDesktop = breakpoints.greaterOrEqual('sm')

const ranges = [
  { label: 'Last 7 days', days: 7 },
  { label: 'Last 14 days', days: 14 },
  { label: 'Last 30 days', days: 30 },
  { label: 'Last 3 months', months: 3 },
  { label: 'Last 6 months', months: 6 },
  { label: 'Last year', years: 1 }
]

const initialEnd = today(tz)
const modelValue = shallowRef({
  start: initialEnd.subtract({ days: 14 }),
  end: initialEnd
})

const label = computed(() => {
  const { start, end } = modelValue.value
  if (!start) return 'Pick a date'
  if (!end) return df.format(start.toDate(tz))
  return `${df.format(start.toDate(tz))} - ${df.format(end.toDate(tz))}`
})

function computeStart(range: typeof ranges[number]) {
  const end = today(tz)
  return { start: end.subtract({ days: range.days, months: range.months, years: range.years }), end }
}

function isRangeSelected(range: typeof ranges[number]) {
  if (!modelValue.value?.start || !modelValue.value?.end) return false
  const { start, end } = computeStart(range)
  return modelValue.value.start.compare(start) === 0 && modelValue.value.end.compare(end) === 0
}

function selectRange(range: typeof ranges[number]) {
  modelValue.value = computeStart(range)
}
</script>

<template>
  <UPopover :content="{ align: 'center' }">
    <UButton color="neutral" variant="subtle" icon="i-lucide-calendar">
      {{ label }}
    </UButton>

    <template #content>
      <div class="flex items-stretch divide-x divide-(--ui-border)">
        <div class="hidden sm:flex flex-col justify-center py-2">
          <UButton
            v-for="(range, index) in ranges"
            :key="index"
            :label="range.label"
            color="neutral"
            variant="ghost"
            class="rounded-none px-4"
            :class="[isRangeSelected(range) ? 'bg-elevated' : 'hover:bg-elevated/50']"
            truncate
            @click="selectRange(range)"
          />
        </div>

        <UCalendar v-model="modelValue" class="p-2" :number-of-months="isDesktop ? 2 : 1" range />
      </div>
    </template>
  </UPopover>
</template>

API

Props

Prop Default Type
as'div'any

The element or component this component should render as.

type'date' "date" | "month" | "year"

The type of picker.

  • date renders a day calendar whose heading can switch to a month then year view.
  • month renders a standalone month picker.
  • year renders a standalone year picker.
nextYearIconappConfig.ui.icons.chevronDoubleRightany

The icon to use for the next year control.

nextYear Omit<ButtonProps, LinkPropsKeys>

Configure the next year button. { color: 'neutral', variant: 'ghost' }

nextMonthIconappConfig.ui.icons.chevronRightany

The icon to use for the next month control.

nextMonth Omit<ButtonProps, LinkPropsKeys>

Configure the next month button. { color: 'neutral', variant: 'ghost' }

prevYearIconappConfig.ui.icons.chevronDoubleLeftany

The icon to use for the previous year control.

prevYear Omit<ButtonProps, LinkPropsKeys>

Configure the prev year button. { color: 'neutral', variant: 'ghost' }

prevMonthIconappConfig.ui.icons.chevronLeftany

The icon to use for the previous month control.

prevMonth Omit<ButtonProps, LinkPropsKeys>

Configure the prev month button. { color: 'neutral', variant: 'ghost' }

viewControltrueboolean | Omit<ButtonProps, LinkPropsKeys>

Whether to make the heading a button that switches between the day, month and year views. Has no effect when type is year. Can be an object to override the button props. { color: 'neutral', variant: 'ghost', block: true }

color'primary' "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral"
variant'solid' "solid" | "outline" | "soft" | "subtle"
size'md' "xs" | "sm" | "md" | "lg" | "xl"
range R

Whether or not a range of dates can be selected

multiple M

Whether or not multiple dates can be selected

monthControlstrueboolean

Show month controls

yearControlstrueboolean

Show year controls

defaultValueCalendarDate | CalendarDateTime | ZonedDateTime | DateRange | DateValue[]
modelValuenull | CalendarDate | CalendarDateTime | ZonedDateTime | DateRange | DateValue[]
weekNumbersboolean
defaultPlaceholderCalendarDate | CalendarDateTime | ZonedDateTime
placeholderCalendarDate | CalendarDateTime | ZonedDateTime
allowNonContiguousRangesboolean

When combined with isDateUnavailable, determines whether non-contiguous ranges, i.e. ranges containing unavailable dates, may be selected.

pagedNavigationboolean

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

preventDeselectboolean

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

maximumDays number

The maximum number of days that can be selected in a range

weekStartsOn 0 | 1 | 2 | 4 | 5 | 3 | 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

fixedWeekstrueboolean

Whether or not to always display 6 weeks in the calendar

maxValueCalendarDate | CalendarDateTime | ZonedDateTime
minValueCalendarDate | CalendarDateTime | ZonedDateTime
locale string

The locale to use for formatting dates

numberOfMonths number

The number of months to display at once

disabledboolean

Whether or not the calendar is disabled

readonlyboolean

Whether or not the calendar is readonly

initialFocusboolean

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

isDateHighlightable (date: DateValue): boolean

A function that returns whether or not a date is hightable

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.

disableDaysOutsideCurrentViewboolean

Whether or not to disable days outside the current view.

fixedDate "start" | "end"

Which part of the range should be fixed

isMonthDisabled (date: DateValue): boolean

A function that returns whether or not a month is disabled

isMonthUnavailable (date: DateValue): boolean

A function that returns whether or not a month is unavailable

isYearDisabled (date: DateValue): boolean

A function that returns whether or not a year is disabled

isYearUnavailable (date: DateValue): boolean

A function that returns whether or not a year is unavailable

ui { root?: SlotClass; header?: SlotClass; body?: SlotClass; heading?: SlotClass; headingLabel?: SlotClass; grid?: SlotClass; gridRow?: SlotClass; gridWeekDaysRow?: SlotClass; gridBody?: SlotClass; headCell?: SlotClass; headCellWeek?: SlotClass; cell?: SlotClass; cellTrigger?: SlotClass; cellWeek?: SlotClass; }

Slots

Slot Type
heading{ value: string; view: CalendarView; date: DateValue; setView: (view: CalendarView) => void; setPlaceholder: (date: DateValue) => void; }
dayPick<CalendarCellTriggerProps, "day">
week-day{ day: string; }
month-cell{ month: DateValue; selected: boolean; disabled: boolean; }
year-cell{ year: DateValue; selected: boolean; disabled: boolean; }

Emits

Event Type
update:modelValue[value: CalendarModelValue<R, M>]
update:placeholder[date: DateValue]
update:validModelValue[date: DateRange]
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: 'flex-1 min-w-0 text-center',
        headingLabel: 'font-medium block truncate p-1.5',
        grid: 'w-full border-collapse select-none space-y-1 focus:outline-none',
        gridRow: 'grid',
        gridWeekDaysRow: 'mb-1 grid w-full grid-cols-7',
        gridBody: 'grid',
        headCell: 'rounded-md',
        headCellWeek: 'rounded-md text-muted',
        cell: 'relative text-center',
        cellTrigger: [
          'm-0.5 relative flex items-center justify-center whitespace-nowrap focus-visible:outline-3 data-disabled:text-muted data-unavailable:line-through data-unavailable:text-muted data-unavailable:pointer-events-none data-today:font-semibold',
          'transition'
        ],
        cellWeek: 'relative text-center text-muted'
      },
      variants: {
        color: {
          primary: {
            headCell: 'text-primary',
            cellTrigger: 'outline-primary/25'
          },
          secondary: {
            headCell: 'text-secondary',
            cellTrigger: 'outline-secondary/25'
          },
          success: {
            headCell: 'text-success',
            cellTrigger: 'outline-success/25'
          },
          info: {
            headCell: 'text-info',
            cellTrigger: 'outline-info/25'
          },
          warning: {
            headCell: 'text-warning',
            cellTrigger: 'outline-warning/25'
          },
          error: {
            headCell: 'text-error',
            cellTrigger: 'outline-error/25'
          },
          neutral: {
            headCell: 'text-highlighted',
            cellTrigger: 'outline-inverted/25'
          }
        },
        variant: {
          solid: '',
          outline: '',
          soft: '',
          subtle: ''
        },
        size: {
          xs: {
            headingLabel: 'text-xs',
            cell: 'text-xs',
            cellWeek: 'text-xs',
            headCell: 'text-[10px]',
            headCellWeek: 'text-[10px]',
            body: 'space-y-2 pt-2'
          },
          sm: {
            headingLabel: 'text-xs',
            headCell: 'text-xs',
            headCellWeek: 'text-xs',
            cellWeek: 'text-xs',
            cell: 'text-xs'
          },
          md: {
            headingLabel: 'text-sm',
            headCell: 'text-xs',
            headCellWeek: 'text-xs',
            cellWeek: 'text-xs',
            cell: 'text-sm'
          },
          lg: {
            headingLabel: 'text-md',
            headCell: 'text-md',
            headCellWeek: 'text-md'
          },
          xl: {
            headingLabel: 'text-lg',
            headCell: 'text-lg',
            headCellWeek: 'text-lg'
          }
        },
        view: {
          day: {
            gridRow: 'grid-cols-7 place-items-center',
            cellTrigger: 'rounded-full data-outside-view:text-muted'
          },
          month: {
            gridRow: 'grid-cols-4',
            cellTrigger: 'rounded-md'
          },
          year: {
            gridRow: 'grid-cols-4',
            cellTrigger: 'rounded-md'
          }
        },
        weekNumbers: {
          true: ''
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          variant: 'solid',
          class: {
            cellTrigger: 'data-selected:bg-primary data-selected:text-inverted data-today:not-data-selected:text-primary data-highlighted:bg-primary/20 hover:not-data-selected:bg-primary/20'
          }
        },
        {
          color: 'primary',
          variant: 'outline',
          class: {
            cellTrigger: 'data-selected:ring data-selected:ring-inset data-selected:ring-primary/50 data-selected:text-primary data-selected:focus-visible:ring-primary data-today:not-data-selected:text-primary data-highlighted:bg-primary/10 hover:not-data-selected:bg-primary/10'
          }
        },
        {
          color: 'primary',
          variant: 'soft',
          class: {
            cellTrigger: 'data-selected:bg-primary/10 data-selected:text-primary data-today:not-data-selected:text-primary data-highlighted:bg-primary/20 hover:not-data-selected:bg-primary/20'
          }
        },
        {
          color: 'primary',
          variant: 'subtle',
          class: {
            cellTrigger: 'data-selected:bg-primary/10 data-selected:text-primary data-selected:ring data-selected:ring-inset data-selected:ring-primary/25 data-selected:focus-visible:ring-primary data-today:not-data-selected:text-primary data-highlighted:bg-primary/20 hover:not-data-selected:bg-primary/20'
          }
        },
        {
          color: 'neutral',
          variant: 'solid',
          class: {
            cellTrigger: 'data-selected:bg-inverted data-selected:text-inverted data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/20 hover:not-data-selected:bg-inverted/10'
          }
        },
        {
          color: 'neutral',
          variant: 'outline',
          class: {
            cellTrigger: 'data-selected:ring data-selected:ring-inset data-selected:ring-accented data-selected:text-default data-selected:bg-default data-selected:focus-visible:ring-inverted data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/10 hover:not-data-selected:bg-inverted/10'
          }
        },
        {
          color: 'neutral',
          variant: 'soft',
          class: {
            cellTrigger: 'data-selected:bg-elevated data-selected:text-default data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/20 hover:not-data-selected:bg-inverted/10'
          }
        },
        {
          color: 'neutral',
          variant: 'subtle',
          class: {
            cellTrigger: 'data-selected:bg-elevated data-selected:text-default data-selected:ring data-selected:ring-inset data-selected:ring-accented data-selected:focus-visible:ring-inverted data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/20 hover:not-data-selected:bg-inverted/10'
          }
        },
        {
          size: 'xs',
          view: 'day',
          class: {
            cellTrigger: 'size-7'
          }
        },
        {
          size: 'sm',
          view: 'day',
          class: {
            cellTrigger: 'size-7'
          }
        },
        {
          size: 'md',
          view: 'day',
          class: {
            cellTrigger: 'size-8'
          }
        },
        {
          size: 'lg',
          view: 'day',
          class: {
            cellTrigger: 'size-9 text-md'
          }
        },
        {
          size: 'xl',
          view: 'day',
          class: {
            cellTrigger: 'size-10 text-lg'
          }
        },
        {
          size: 'xs',
          view: [
            'month',
            'year'
          ],
          class: {
            cellTrigger: 'h-7 px-2'
          }
        },
        {
          size: 'sm',
          view: [
            'month',
            'year'
          ],
          class: {
            cellTrigger: 'h-7 px-2'
          }
        },
        {
          size: 'md',
          view: [
            'month',
            'year'
          ],
          class: {
            cellTrigger: 'h-8 px-3'
          }
        },
        {
          size: 'lg',
          view: [
            'month',
            'year'
          ],
          class: {
            cellTrigger: 'h-9 px-4 text-md'
          }
        },
        {
          size: 'xl',
          view: [
            'month',
            'year'
          ],
          class: {
            cellTrigger: 'h-10 px-5 text-lg'
          }
        },
        {
          view: 'day',
          weekNumbers: true,
          class: {
            gridRow: 'grid-cols-8',
            gridWeekDaysRow: 'grid-cols-8 [&>*:first-child]:col-start-2'
          }
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        variant: 'solid',
        view: 'day'
      }
    }
  }
})
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: 'flex-1 min-w-0 text-center',
            headingLabel: 'font-medium block truncate p-1.5',
            grid: 'w-full border-collapse select-none space-y-1 focus:outline-none',
            gridRow: 'grid',
            gridWeekDaysRow: 'mb-1 grid w-full grid-cols-7',
            gridBody: 'grid',
            headCell: 'rounded-md',
            headCellWeek: 'rounded-md text-muted',
            cell: 'relative text-center',
            cellTrigger: [
              'm-0.5 relative flex items-center justify-center whitespace-nowrap focus-visible:outline-3 data-disabled:text-muted data-unavailable:line-through data-unavailable:text-muted data-unavailable:pointer-events-none data-today:font-semibold',
              'transition'
            ],
            cellWeek: 'relative text-center text-muted'
          },
          variants: {
            color: {
              primary: {
                headCell: 'text-primary',
                cellTrigger: 'outline-primary/25'
              },
              secondary: {
                headCell: 'text-secondary',
                cellTrigger: 'outline-secondary/25'
              },
              success: {
                headCell: 'text-success',
                cellTrigger: 'outline-success/25'
              },
              info: {
                headCell: 'text-info',
                cellTrigger: 'outline-info/25'
              },
              warning: {
                headCell: 'text-warning',
                cellTrigger: 'outline-warning/25'
              },
              error: {
                headCell: 'text-error',
                cellTrigger: 'outline-error/25'
              },
              neutral: {
                headCell: 'text-highlighted',
                cellTrigger: 'outline-inverted/25'
              }
            },
            variant: {
              solid: '',
              outline: '',
              soft: '',
              subtle: ''
            },
            size: {
              xs: {
                headingLabel: 'text-xs',
                cell: 'text-xs',
                cellWeek: 'text-xs',
                headCell: 'text-[10px]',
                headCellWeek: 'text-[10px]',
                body: 'space-y-2 pt-2'
              },
              sm: {
                headingLabel: 'text-xs',
                headCell: 'text-xs',
                headCellWeek: 'text-xs',
                cellWeek: 'text-xs',
                cell: 'text-xs'
              },
              md: {
                headingLabel: 'text-sm',
                headCell: 'text-xs',
                headCellWeek: 'text-xs',
                cellWeek: 'text-xs',
                cell: 'text-sm'
              },
              lg: {
                headingLabel: 'text-md',
                headCell: 'text-md',
                headCellWeek: 'text-md'
              },
              xl: {
                headingLabel: 'text-lg',
                headCell: 'text-lg',
                headCellWeek: 'text-lg'
              }
            },
            view: {
              day: {
                gridRow: 'grid-cols-7 place-items-center',
                cellTrigger: 'rounded-full data-outside-view:text-muted'
              },
              month: {
                gridRow: 'grid-cols-4',
                cellTrigger: 'rounded-md'
              },
              year: {
                gridRow: 'grid-cols-4',
                cellTrigger: 'rounded-md'
              }
            },
            weekNumbers: {
              true: ''
            }
          },
          compoundVariants: [
            {
              color: 'primary',
              variant: 'solid',
              class: {
                cellTrigger: 'data-selected:bg-primary data-selected:text-inverted data-today:not-data-selected:text-primary data-highlighted:bg-primary/20 hover:not-data-selected:bg-primary/20'
              }
            },
            {
              color: 'primary',
              variant: 'outline',
              class: {
                cellTrigger: 'data-selected:ring data-selected:ring-inset data-selected:ring-primary/50 data-selected:text-primary data-selected:focus-visible:ring-primary data-today:not-data-selected:text-primary data-highlighted:bg-primary/10 hover:not-data-selected:bg-primary/10'
              }
            },
            {
              color: 'primary',
              variant: 'soft',
              class: {
                cellTrigger: 'data-selected:bg-primary/10 data-selected:text-primary data-today:not-data-selected:text-primary data-highlighted:bg-primary/20 hover:not-data-selected:bg-primary/20'
              }
            },
            {
              color: 'primary',
              variant: 'subtle',
              class: {
                cellTrigger: 'data-selected:bg-primary/10 data-selected:text-primary data-selected:ring data-selected:ring-inset data-selected:ring-primary/25 data-selected:focus-visible:ring-primary data-today:not-data-selected:text-primary data-highlighted:bg-primary/20 hover:not-data-selected:bg-primary/20'
              }
            },
            {
              color: 'neutral',
              variant: 'solid',
              class: {
                cellTrigger: 'data-selected:bg-inverted data-selected:text-inverted data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/20 hover:not-data-selected:bg-inverted/10'
              }
            },
            {
              color: 'neutral',
              variant: 'outline',
              class: {
                cellTrigger: 'data-selected:ring data-selected:ring-inset data-selected:ring-accented data-selected:text-default data-selected:bg-default data-selected:focus-visible:ring-inverted data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/10 hover:not-data-selected:bg-inverted/10'
              }
            },
            {
              color: 'neutral',
              variant: 'soft',
              class: {
                cellTrigger: 'data-selected:bg-elevated data-selected:text-default data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/20 hover:not-data-selected:bg-inverted/10'
              }
            },
            {
              color: 'neutral',
              variant: 'subtle',
              class: {
                cellTrigger: 'data-selected:bg-elevated data-selected:text-default data-selected:ring data-selected:ring-inset data-selected:ring-accented data-selected:focus-visible:ring-inverted data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/20 hover:not-data-selected:bg-inverted/10'
              }
            },
            {
              size: 'xs',
              view: 'day',
              class: {
                cellTrigger: 'size-7'
              }
            },
            {
              size: 'sm',
              view: 'day',
              class: {
                cellTrigger: 'size-7'
              }
            },
            {
              size: 'md',
              view: 'day',
              class: {
                cellTrigger: 'size-8'
              }
            },
            {
              size: 'lg',
              view: 'day',
              class: {
                cellTrigger: 'size-9 text-md'
              }
            },
            {
              size: 'xl',
              view: 'day',
              class: {
                cellTrigger: 'size-10 text-lg'
              }
            },
            {
              size: 'xs',
              view: [
                'month',
                'year'
              ],
              class: {
                cellTrigger: 'h-7 px-2'
              }
            },
            {
              size: 'sm',
              view: [
                'month',
                'year'
              ],
              class: {
                cellTrigger: 'h-7 px-2'
              }
            },
            {
              size: 'md',
              view: [
                'month',
                'year'
              ],
              class: {
                cellTrigger: 'h-8 px-3'
              }
            },
            {
              size: 'lg',
              view: [
                'month',
                'year'
              ],
              class: {
                cellTrigger: 'h-9 px-4 text-md'
              }
            },
            {
              size: 'xl',
              view: [
                'month',
                'year'
              ],
              class: {
                cellTrigger: 'h-10 px-5 text-lg'
              }
            },
            {
              view: 'day',
              weekNumbers: true,
              class: {
                gridRow: 'grid-cols-8',
                gridWeekDaysRow: 'grid-cols-8 [&>*:first-child]:col-start-2'
              }
            }
          ],
          defaultVariants: {
            size: 'md',
            color: 'primary',
            variant: 'solid',
            view: 'day'
          }
        }
      }
    })
  ]
})
Some colors in compoundVariants are omitted for readability. Check out the source code on GitHub.

Changelog

No recent changes