Usage
Use the v-model directive to control the selected date.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
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(new CalendarDate(2022, 2, 3))
</script>
<template>
<PCalendar 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>
<PCalendar v-model="value" />
</template>Use the default-value prop to set the initial value when you do not need to control its state.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
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 defaultValue = shallowRef(new CalendarDate(2022, 2, 6))
</script>
<template>
<PCalendar :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>
<PCalendar :default-value="defaultValue" />
</template>@internationalized/date package for locale-aware formatting. The date format is determined by the locale prop of the App component.@internationalized/date package for locale-aware formatting. The date format is determined by the locale prop of the App component.Type
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 |
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'
const value = shallowRef(new CalendarDate(2022, 2, 1))
</script>
<template>
<PCalendar 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>
<PCalendar type="month" v-model="value" />
</template>Use type="year" to render a standalone year picker.
2020 | 2021 | 2022 | 2023 |
2024 | 2025 | 2026 | 2027 |
2028 | 2029 | 2030 | 2031 |
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'
const value = shallowRef(new CalendarDate(2022, 1, 1))
</script>
<template>
<PCalendar 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>
<PCalendar type="year" v-model="value" />
</template>Multiple
Use the multiple prop to allow multiple selections.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
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([
new CalendarDate(2022, 2, 4),
new CalendarDate(2022, 2, 6),
new CalendarDate(2022, 2, 8)
])
</script>
<template>
<PCalendar 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>
<PCalendar multiple v-model="value" />
</template>Range
Use the range prop to select a range of dates.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
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>
<PCalendar 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>
<PCalendar 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.
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>
<PCalendar 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>
<PCalendar type="month" range v-model="value" />
</template>Number Of Months
Use the numberOfMonths prop to change the number of months in the calendar.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 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 |
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
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 | 11 | 12 |
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
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 | 9 |
<template>
<PCalendar :number-of-months="3" />
</template>Month Controls
Use the month-controls prop to show the month controls. Defaults to true.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 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 |
<template>
<PCalendar />
</template>Use the prev-month and next-month props to override the month buttons.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 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 |
<template>
<PCalendar
: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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 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 |
<template>
<PCalendar />
</template>Use the prev-year and next-year props to override the year buttons.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 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 |
<template>
<PCalendar
:prev-year="{
color: 'primary',
variant: 'soft'
}"
:next-year="{
color: 'primary',
variant: 'soft'
}"
/>
</template>View Control
Use the view-control prop to make the heading a button that switches between the day, month and year views. Defaults to true.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 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 |
<template>
<PCalendar />
</template>Set the view-control prop to an object to override the heading button.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 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 |
<template>
<PCalendar
:view-control="{
color: 'primary',
variant: 'soft'
}"
/>
</template>Years Per Page
Use the years-per-page prop to change the number of years displayed in the year view. Defaults to 12.
2020 | 2021 | 2022 | 2023 |
2024 | 2025 | 2026 | 2027 |
2028 | 2029 | 2030 | 2031 |
2032 | 2033 | 2034 | 2035 |
2036 | 2037 | 2038 | 2039 |
<template>
<PCalendar type="year" :years-per-page="20" />
</template>Fixed Weeks
Use the fixed-weeks prop to display the calendar with fixed weeks.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 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 |
<template>
<PCalendar />
</template>Week Numbers
Use the week-numbers prop to display week numbers in the calendar.
| S | M | T | W | T | F | S | |
|---|---|---|---|---|---|---|---|
| 40 | 27 | 28 | 29 | 30 | 1 | 2 | 3 |
| 41 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 42 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 43 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 44 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
| 45 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
<template>
<PCalendar week-numbers fixed-weeks />
</template>Color
Use the color prop to change the color of the calendar.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
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>
<PCalendar color="neutral" />
</template>Variant
Use the variant prop to change the variant of the calendar.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
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>
<PCalendar variant="subtle" />
</template>Size
Use the size prop to change the size of the calendar.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 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 |
<template>
<PCalendar size="xl" />
</template>Disabled
Use the disabled prop to disable the calendar.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 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 |
<template>
<PCalendar disabled />
</template>Examples
With chip events
Use the Chip component to add events to specific days.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
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';
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>
<PCalendar v-model="modelValue">
<template #day="{ day }">
<PChip
:show="!!getColorByDate(day.toDate('UTC'))"
:color="getColorByDate(day.toDate('UTC'))"
size="2xs"
>
{{ day.day }}
</PChip>
</template>
</PCalendar>
</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>
<PCalendar v-model="modelValue">
<template #day="{ day }">
<PChip
:show="!!getColorByDate(day.toDate('UTC'))"
:color="getColorByDate(day.toDate('UTC'))"
size="2xs"
>
{{ day.day }}
</PChip>
</template>
</PCalendar>
</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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
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),
});
function isDateDisabled(date: DateValue) {
return date.day >= 10 && date.day <= 16;
}
</script>
<template>
<PCalendar
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),
});
function isDateDisabled(date: DateValue) {
return date.day >= 10 && date.day <= 16;
}
</script>
<template>
<PCalendar
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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
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),
});
function isDateUnavailable(date: DateValue) {
return date.day >= 10 && date.day <= 16;
}
</script>
<template>
<PCalendar
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),
});
function isDateUnavailable(date: DateValue) {
return date.day >= 10 && date.day <= 16;
}
</script>
<template>
<PCalendar
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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
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 |
<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>
<PCalendar
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>
<PCalendar
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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
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 |
<script lang="ts" setup>
import { CalendarDate, HebrewCalendar } from '@internationalized/date';
const hebrewDate = shallowRef(new CalendarDate(new HebrewCalendar(), 5781, 1, 1));
</script>
<template>
<PCalendar v-model="hebrewDate" />
</template>
With external controls
You can control the calendar with external controls by manipulating the date passed in the v-model.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
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 |
<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">
<PCalendar
v-model="date"
:month-controls="false"
:year-controls="false"
/>
<div class="flex gap-4 justify-between">
<PButton
color="neutral"
variant="outline"
@click="date = date.subtract({ months: 1 })"
>
Prev
</PButton>
<PButton
color="neutral"
variant="outline"
@click="date = date.add({ months: 1 })"
>
Next
</PButton>
</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">
<PCalendar
v-model="date"
:month-controls="false"
:year-controls="false"
/>
<div class="flex gap-4 justify-between">
<PButton
color="neutral"
variant="outline"
@click="date = date.subtract({ months: 1 })"
>
Prev
</PButton>
<PButton
color="neutral"
variant="outline"
@click="date = date.add({ months: 1 })"
>
Next
</PButton>
</div>
</div>
</template>
With today's date
Use the today function from @internationalized/date with getLocalTimeZone to set the value to the current date.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 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 |
<script setup lang="ts">
import { getLocalTimeZone, today } from '@internationalized/date';
const date = shallowRef(today(getLocalTimeZone()));
</script>
<template>
<div class="flex flex-col gap-4">
<PCalendar v-model="date" />
<PButton
color="neutral"
variant="outline"
class="justify-center"
@click="date = today(getLocalTimeZone())"
>
Today
</PButton>
</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">
<PCalendar v-model="date" />
<PButton
color="neutral"
variant="outline"
class="justify-center"
@click="date = today(getLocalTimeZone())"
>
Today
</PButton>
</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>
<PPopover>
<PButton
color="neutral"
variant="subtle"
icon="i-lucide-calendar"
>
{{ modelValue ? df.format(modelValue.toDate(getLocalTimeZone())) : 'Select a date' }}
</PButton>
<template #content>
<PCalendar
v-model="modelValue"
class="p-2"
/>
</template>
</PPopover>
</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>
<PPopover>
<PButton
color="neutral"
variant="subtle"
icon="i-lucide-calendar"
>
{{ modelValue ? df.format(modelValue.toDate(getLocalTimeZone())) : 'Select a date' }}
</PButton>
<template #content>
<PCalendar
v-model="modelValue"
class="p-2"
/>
</template>
</PPopover>
</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 { DateFormatter, getLocalTimeZone, today } from '@internationalized/date';
import { useBreakpoints } from '@vueuse/core';
const df = new DateFormatter('en-US', { dateStyle: 'medium' });
const tz = getLocalTimeZone();
const breakpoints = useBreakpoints({ 'sm': 640, 'md': 768, 'lg': 1024, 'xl': 1280, '2xl': 1536 });
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>
<PPopover :content="{ align: 'center' }">
<PButton
color="neutral"
variant="subtle"
icon="i-lucide-calendar"
>
{{ label }}
</PButton>
<template #content>
<div class="divide---ui-color-border flex items-stretch divide-(x)">
<div class="py-2 flex-col hidden justify-center sm:flex">
<PButton
v-for="(range, index) in ranges"
:key="index"
:label="range.label"
color="neutral"
variant="ghost"
class="px-4 rounded-none"
:class="[isRangeSelected(range) ? 'bg-background-elevated' : 'hover:bg-background-elevated/50']"
truncate
@click="selectRange(range)"
/>
</div>
<PCalendar
v-model="modelValue"
class="p-2"
:number-of-months="isDesktop ? 2 : 1"
range
/>
</div>
</template>
</PPopover>
</template>
<script setup lang="ts">
import { shallowRef, computed } from 'vue'
import { DateFormatter, getLocalTimeZone, today } from '@internationalized/date';
import { useBreakpoints } from '@vueuse/core';
const df = new DateFormatter('en-US', { dateStyle: 'medium' });
const tz = getLocalTimeZone();
const breakpoints = useBreakpoints({ 'sm': 640, 'md': 768, 'lg': 1024, 'xl': 1280, '2xl': 1536 });
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>
<PPopover :content="{ align: 'center' }">
<PButton
color="neutral"
variant="subtle"
icon="i-lucide-calendar"
>
{{ label }}
</PButton>
<template #content>
<div class="divide---ui-color-border flex items-stretch divide-(x)">
<div class="py-2 flex-col hidden justify-center sm:flex">
<PButton
v-for="(range, index) in ranges"
:key="index"
:label="range.label"
color="neutral"
variant="ghost"
class="px-4 rounded-none"
:class="[isRangeSelected(range) ? 'bg-background-elevated' : 'hover:bg-background-elevated/50']"
truncate
@click="selectRange(range)"
/>
</div>
<PCalendar
v-model="modelValue"
class="p-2"
:number-of-months="isDesktop ? 2 : 1"
range
/>
</div>
</template>
</PPopover>
</template>
API
Props
| Prop | Default | Type |
|---|
Slots
| Slot | Type |
|---|
Emits
| Event | Type |
|---|
Theme
export default defineAppConfig({
ui: {
calendar: {
slots: {
root: '',
header: 'flex items-center justify-between',
body: 'pt-4 flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0',
heading: 'flex-1 min-w-0 text-center',
headingLabel: 'font-500 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 color-text-muted',
cell: 'relative text-center',
cellTrigger: [
'data-[disabled]:color-text-muted data-[unavailable]:color-text-muted m-0.5 flex whitespace-nowrap items-center justify-center relative data-[today]:font-600 focus-visible:outline-3 data-[unavailable]:line-through data-[unavailable]:pointer-events-none',
'transition'
],
cellWeek: 'relative text-center color-text-muted'
},
variants: {
color: {
primary: {
headCell: 'color-primary',
cellTrigger: 'outline-primary/25'
},
secondary: {
headCell: 'color-secondary',
cellTrigger: 'outline-secondary/25'
},
success: {
headCell: 'color-success',
cellTrigger: 'outline-success/25'
},
info: {
headCell: 'color-info',
cellTrigger: 'outline-info/25'
},
warning: {
headCell: 'color-warning',
cellTrigger: 'outline-warning/25'
},
error: {
headCell: 'color-error',
cellTrigger: 'outline-error/25'
},
neutral: {
headCell: 'color-text-highlighted',
cellTrigger: 'outline-border-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-base',
headCell: 'text-base',
headCellWeek: 'text-base',
cellWeek: 'text-base',
cell: 'text-base'
},
xl: {
headingLabel: 'text-lg',
headCell: 'text-lg',
headCellWeek: 'text-lg',
cellWeek: 'text-lg',
cell: 'text-lg'
}
},
view: {
day: {
gridRow: 'grid-cols-7 place-items-center',
cellTrigger: 'data-[outside-view]:color-text-muted rounded-full'
},
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]:color-primary-foreground data-[today]:not-data-[selected]:color-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]:color-primary data-[today]:not-data-[selected]:color-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]:color-primary data-[today]:not-data-[selected]:color-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]:color-primary data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-primary/25 data-[today]:not-data-[selected]:color-primary data-[highlighted]:bg-primary/20 hover:not-data-[selected]:bg-primary/20'
}
},
{
color: 'neutral',
variant: 'solid',
class: {
cellTrigger: 'data-[selected]:bg-background-inverted data-[selected]:color-text-inverted data-[today]:not-data-[selected]:color-text-highlighted data-[highlighted]:bg-background-inverted/20 hover:not-data-[selected]:bg-background-inverted/10'
}
},
{
color: 'neutral',
variant: 'outline',
class: {
cellTrigger: 'data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-border-accented data-[selected]:color-text data-[selected]:bg-background data-[today]:not-data-[selected]:color-text-highlighted data-[highlighted]:bg-background-inverted/10 hover:not-data-[selected]:bg-background-inverted/10'
}
},
{
color: 'neutral',
variant: 'soft',
class: {
cellTrigger: 'data-[selected]:bg-background-elevated data-[selected]:color-text data-[today]:not-data-[selected]:color-text-highlighted data-[highlighted]:bg-background-inverted/20 hover:not-data-[selected]:bg-background-inverted/10'
}
},
{
color: 'neutral',
variant: 'subtle',
class: {
cellTrigger: 'data-[selected]:bg-background-elevated data-[selected]:color-text data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-border-accented data-[today]:not-data-[selected]:color-text-highlighted data-[highlighted]:bg-background-inverted/20 hover:not-data-[selected]:bg-background-inverted/10'
}
},
{
size: 'xs',
view: 'day',
class: {
cellTrigger: 'size-6'
}
},
{
size: 'sm',
view: 'day',
class: {
cellTrigger: 'size-7'
}
},
{
size: 'md',
view: 'day',
class: {
cellTrigger: 'size-8'
}
},
{
size: 'lg',
view: 'day',
class: {
cellTrigger: 'size-9'
}
},
{
size: 'xl',
view: 'day',
class: {
cellTrigger: 'size-10'
}
},
{
size: 'xs',
view: [
'month',
'year'
],
class: {
cellTrigger: 'h-6 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'
}
},
{
size: 'xl',
view: [
'month',
'year'
],
class: {
cellTrigger: 'h-10 px-5'
}
},
{
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'
}
}
}
})import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from 'pohon-ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
calendar: {
slots: {
root: '',
header: 'flex items-center justify-between',
body: 'pt-4 flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0',
heading: 'flex-1 min-w-0 text-center',
headingLabel: 'font-500 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 color-text-muted',
cell: 'relative text-center',
cellTrigger: [
'data-[disabled]:color-text-muted data-[unavailable]:color-text-muted m-0.5 flex whitespace-nowrap items-center justify-center relative data-[today]:font-600 focus-visible:outline-3 data-[unavailable]:line-through data-[unavailable]:pointer-events-none',
'transition'
],
cellWeek: 'relative text-center color-text-muted'
},
variants: {
color: {
primary: {
headCell: 'color-primary',
cellTrigger: 'outline-primary/25'
},
secondary: {
headCell: 'color-secondary',
cellTrigger: 'outline-secondary/25'
},
success: {
headCell: 'color-success',
cellTrigger: 'outline-success/25'
},
info: {
headCell: 'color-info',
cellTrigger: 'outline-info/25'
},
warning: {
headCell: 'color-warning',
cellTrigger: 'outline-warning/25'
},
error: {
headCell: 'color-error',
cellTrigger: 'outline-error/25'
},
neutral: {
headCell: 'color-text-highlighted',
cellTrigger: 'outline-border-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-base',
headCell: 'text-base',
headCellWeek: 'text-base',
cellWeek: 'text-base',
cell: 'text-base'
},
xl: {
headingLabel: 'text-lg',
headCell: 'text-lg',
headCellWeek: 'text-lg',
cellWeek: 'text-lg',
cell: 'text-lg'
}
},
view: {
day: {
gridRow: 'grid-cols-7 place-items-center',
cellTrigger: 'data-[outside-view]:color-text-muted rounded-full'
},
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]:color-primary-foreground data-[today]:not-data-[selected]:color-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]:color-primary data-[today]:not-data-[selected]:color-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]:color-primary data-[today]:not-data-[selected]:color-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]:color-primary data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-primary/25 data-[today]:not-data-[selected]:color-primary data-[highlighted]:bg-primary/20 hover:not-data-[selected]:bg-primary/20'
}
},
{
color: 'neutral',
variant: 'solid',
class: {
cellTrigger: 'data-[selected]:bg-background-inverted data-[selected]:color-text-inverted data-[today]:not-data-[selected]:color-text-highlighted data-[highlighted]:bg-background-inverted/20 hover:not-data-[selected]:bg-background-inverted/10'
}
},
{
color: 'neutral',
variant: 'outline',
class: {
cellTrigger: 'data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-border-accented data-[selected]:color-text data-[selected]:bg-background data-[today]:not-data-[selected]:color-text-highlighted data-[highlighted]:bg-background-inverted/10 hover:not-data-[selected]:bg-background-inverted/10'
}
},
{
color: 'neutral',
variant: 'soft',
class: {
cellTrigger: 'data-[selected]:bg-background-elevated data-[selected]:color-text data-[today]:not-data-[selected]:color-text-highlighted data-[highlighted]:bg-background-inverted/20 hover:not-data-[selected]:bg-background-inverted/10'
}
},
{
color: 'neutral',
variant: 'subtle',
class: {
cellTrigger: 'data-[selected]:bg-background-elevated data-[selected]:color-text data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-border-accented data-[today]:not-data-[selected]:color-text-highlighted data-[highlighted]:bg-background-inverted/20 hover:not-data-[selected]:bg-background-inverted/10'
}
},
{
size: 'xs',
view: 'day',
class: {
cellTrigger: 'size-6'
}
},
{
size: 'sm',
view: 'day',
class: {
cellTrigger: 'size-7'
}
},
{
size: 'md',
view: 'day',
class: {
cellTrigger: 'size-8'
}
},
{
size: 'lg',
view: 'day',
class: {
cellTrigger: 'size-9'
}
},
{
size: 'xl',
view: 'day',
class: {
cellTrigger: 'size-10'
}
},
{
size: 'xs',
view: [
'month',
'year'
],
class: {
cellTrigger: 'h-6 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'
}
},
{
size: 'xl',
view: [
'month',
'year'
],
class: {
cellTrigger: 'h-10 px-5'
}
},
{
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'
}
}
}
})
]
})