Calendar

Date picker with month grid, navigation, and year picker support built on Reka UI's Calendar.

Usage

<script setup lang="ts">
import { Calendar } from '@hareui/vue'
</script>
October 2026
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
Event date, October 2026
<script setup lang="ts">import { Calendar } from '@hareui/vue'</script><template>  <Calendar aria-label="Event date" /></template>

Anatomy

Calendar renders a header (heading or year-picker trigger, plus previous/next buttons) followed by a 7-column day grid, with an optional overlay year grid.

<template>
  <Calendar aria-label="Event date">
    <template #navIcon="{ direction }" />                                  <!-- replaces both chevrons -->
    <template #cell="{ date, today, selected, disabled, unavailable, outsideView }" /> <!-- replaces formatted day -->
  </Calendar>
</template>
ui keydata-slotElement
basecalendarroot element
headercalendar-headerrow containing the heading/year-picker trigger and nav buttons
headingcalendar-headingformatted month/year label (hidden when year-picker)
navButtoncalendar-nav-buttonprevious/next button
navButtonIconcalendar-nav-button-icondefault chevron inside a nav button
gridcalendar-gridthe day grid
gridHeadercalendar-grid-headerweekday label row wrapper
gridBodycalendar-grid-bodyweek rows wrapper
gridRow–one week row
headerCellcalendar-header-cellweekday label cell
cellcalendar-cellone interactive day
cellIndicatorcalendar-cell-indicatordot rendered when indicator(date) is true
yearPickerTriggercalendar-year-picker-triggertoggles the year grid (when year-picker)
yearPickerTriggerHeadingcalendar-year-picker-trigger-headingheading text inside the trigger
yearPickerTriggerIndicatorcalendar-year-picker-trigger-indicatorchevron icon inside the trigger
yearPickerGridcalendar-year-picker-gridoverlay grid of selectable years
yearPickerYearCellcalendar-year-picker-year-cellone selectable year

Examples

Default Value

February 2025
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
Event date, February 2025
<script setup lang="ts">import { Calendar } from '@hareui/vue'import { parseDate } from '@internationalized/date'</script><template>  <Calendar aria-label="Event date" :default-value="parseDate('2025-02-14')" /></template>

Disabled

October 2026
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
Event date, October 2026
Calendar is disabled
<script setup lang="ts">import { Calendar, Description } from '@hareui/vue'import { getLocalTimeZone, today } from '@internationalized/date'</script><template>  <div class="flex flex-col items-center gap-4">    <Calendar aria-label="Event date" :default-value="today(getLocalTimeZone())" disabled />    <Description class="text-center">      Calendar is disabled    </Description>  </div></template>

Read Only

October 2026
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
Event date, October 2026
Calendar is read-only
<script setup lang="ts">import { Calendar, Description } from '@hareui/vue'import { getLocalTimeZone, today } from '@internationalized/date'</script><template>  <div class="flex flex-col items-center gap-4">    <Calendar aria-label="Event date" :default-value="today(getLocalTimeZone())" readonly />    <Description class="text-center">      Calendar is read-only    </Description>  </div></template>

Year Picker

Set year-picker to replace the plain heading with a toggleable overlay grid of years, bounded by min-value/max-value (defaulting to 1900–2099).

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
Event date, October 2026
<script setup lang="ts">import { Calendar } from '@hareui/vue'</script><template>  <Calendar aria-label="Event date" year-picker /></template>

Controlled

Use v-model and v-model:placeholder for external state coordination and custom shortcuts.

December 2025
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
Event date, December 2025
Selected date: (none)
<script setup lang="ts">import type { DateValue } from '@internationalized/date'import { Button, ButtonGroup, Calendar, Description } from '@hareui/vue'import { getLocalTimeZone, parseDate, startOfMonth, startOfWeek, today } from '@internationalized/date'import { shallowRef } from 'vue'const value = shallowRef<DateValue | null>(null)const focusedDate = shallowRef<DateValue>(parseDate('2025-12-25'))</script><template>  <div class="flex flex-col items-center gap-4">    <ButtonGroup full-width size="sm" variant="tertiary">      <Button        @click="() => {          const todayDate = today(getLocalTimeZone())          value = todayDate          focusedDate = todayDate        }"      >        Today      </Button>      <Button        @click="() => {          const nextWeekStart = startOfWeek(today(getLocalTimeZone()), 'en-US')          value = nextWeekStart          focusedDate = nextWeekStart        }"      >        Week      </Button>      <Button        @click="() => {          const nextMonthStart = startOfMonth(today(getLocalTimeZone()))          value = nextMonthStart          focusedDate = nextMonthStart        }"      >        Month      </Button>    </ButtonGroup>    <Calendar aria-label="Event date" v-model="value" v-model:placeholder="focusedDate" />    <Description class="text-center">      Selected date: {{ value ? value.toString() : '(none)' }}    </Description>    <div class="flex gap-2">      <Button        size="sm"        variant="secondary"        @click="() => {          const todayDate = today(getLocalTimeZone())          value = todayDate          focusedDate = todayDate        }"      >        Set Today      </Button>      <Button        size="sm"        variant="secondary"        @click="() => {          const christmasDate = parseDate('2025-12-25')          value = christmasDate          focusedDate = christmasDate        }"      >        Set Christmas      </Button>      <Button size="sm" variant="tertiary" @click="value = null">        Clear      </Button>    </div>  </div></template>

Min and Max Dates

October 2026
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
Appointment date, October 2026
Select a date between today and 2027-01-03
<script setup lang="ts">import { Calendar, Description } from '@hareui/vue'import { getLocalTimeZone, today } from '@internationalized/date'const now = today(getLocalTimeZone())const minDate = nowconst maxDate = now.add({ months: 3 })</script><template>  <div class="flex flex-col items-center gap-4">    <Calendar aria-label="Appointment date" :min-value="minDate" :max-value="maxDate" />    <Description class="text-center">      Select a date between today and {{ maxDate.toString() }}    </Description>  </div></template>

Unavailable Dates

Use is-date-unavailable to block dates such as weekends, holidays, or booked slots.

October 2026
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
Appointment date, October 2026
Weekends are unavailable
<script setup lang="ts">import type { DateValue } from '@internationalized/date'import { Calendar, Description } from '@hareui/vue'import { isWeekend } from '@internationalized/date'function isDateUnavailable(date: DateValue) {  return isWeekend(date, 'en-US')}</script><template>  <div class="flex flex-col items-center gap-4">    <Calendar aria-label="Appointment date" :is-date-unavailable="isDateUnavailable" />    <Description class="text-center">      Weekends are unavailable    </Description>  </div></template>

Weeks in Month

Set fixed-weeks to always render 6 weeks, keeping the grid height stable when navigating between months.

October 2026
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
Event date, October 2026
Always shows 6 weeks per month to avoid layout shift when navigating
<script setup lang="ts">import { Calendar, Description } from '@hareui/vue'</script><template>  <div class="flex flex-col items-center gap-4">    <Calendar aria-label="Event date" fixed-weeks />    <Description class="text-center">      Always shows 6 weeks per month to avoid layout shift when navigating    </Description>  </div></template>

Week View

Set visible-duration to { weeks: n } to show one or more weeks at a time. Navigation advances by the visible week range. Use page-behavior="single" to move one week at a time when showing multiple weeks.

Visible weeks
27
28
29
30
1
2
3
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Calendar, Select } from '@hareui/vue'import { ref } from 'vue'const weekOptions: ListBoxItem[] = [  { value: '1', label: '1 week' },  { value: '2', label: '2 weeks' },  { value: '3', label: '3 weeks' },  { value: '4', label: '4 weeks' },  { value: '5', label: '5 weeks' },  { value: '6', label: '6 weeks' },  { value: '8', label: '8 weeks' },]const weeks = ref(1)</script><template>  <div class="flex flex-col items-center gap-6">    <Select      class="w-40"      label="Visible weeks"      :items="weekOptions"      :model-value="String(weeks)"      @update:model-value="(value) => value && (weeks = Number(value))"    />    <Calendar aria-label="Week view" :visible-duration="{ weeks }" />  </div></template>

Day View

Set visible-duration to { days: n } to show a rolling window of consecutive days. Navigation advances by the visible day range. Use page-behavior="single" to move one day at a time when showing multiple days.

Visible days
1
2
3
4
5
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Calendar, Select } from '@hareui/vue'import { ref } from 'vue'const dayOptions: ListBoxItem[] = [  { value: '1', label: '1 day' },  { value: '5', label: '5 days' },  { value: '7', label: '7 days' },  { value: '8', label: '8 days' },  { value: '10', label: '10 days' },  { value: '14', label: '14 days' },  { value: '21', label: '21 days' },]const days = ref(5)</script><template>  <div class="flex flex-col items-center gap-6">    <Select      class="w-40"      label="Visible days"      :items="dayOptions"      :model-value="String(days)"      @update:model-value="(value) => value && (days = Number(value))"    />    <Calendar aria-label="Day view" :visible-duration="{ days }" />  </div></template>

Multiple Selection

Set selection-mode="multiple" to let users select several dates. model-value and default-value become an array of dates.

January 1900
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
Event dates, January 1900
Select multiple dates
<script setup lang="ts">import type { DateValue } from '@internationalized/date'import { Calendar, Description } from '@hareui/vue'import { shallowRef } from 'vue'const value = shallowRef<DateValue[]>([])</script><template>  <div class="flex flex-col items-center gap-4">    <Calendar      aria-label="Event dates"      selection-mode="multiple"      :model-value="value"      @update:model-value="v => value = v as DateValue[]"    />    <Description class="text-center">      {{ value.length ? `${value.length} date(s) selected` : 'Select multiple dates' }}    </Description>  </div></template>

Focused Value

Programmatically control which month is displayed using v-model:placeholder.

June 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
1
2
3
4
5
Event date, June 2025
Focused: 2025-06-15
<script setup lang="ts">import type { DateValue } from '@internationalized/date'import { Button, Calendar, Description } from '@hareui/vue'import { parseDate } from '@internationalized/date'import { shallowRef } from 'vue'const focusedDate = shallowRef<DateValue>(parseDate('2025-06-15'))</script><template>  <div class="flex flex-col items-center gap-4">    <Calendar aria-label="Event date" v-model:placeholder="focusedDate" />    <Description class="text-center">      Focused: {{ focusedDate.toString() }}    </Description>    <div class="flex flex-wrap justify-center gap-2">      <Button size="sm" variant="secondary" @click="focusedDate = parseDate('2025-01-01')">        Go to Jan      </Button>      <Button size="sm" variant="secondary" @click="focusedDate = parseDate('2025-06-15')">        Go to Jun      </Button>      <Button size="sm" variant="secondary" @click="focusedDate = parseDate('2025-12-25')">        Go to Christmas      </Button>    </div>  </div></template>

Cell Indicators

Pass indicator to render a dot under matching dates, e.g. to surface metadata like events.

October 2026
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
Event date, October 2026
<script setup lang="ts">import type { DateValue } from '@internationalized/date'import { getLocalTimeZone, isToday } from '@internationalized/date'import { Calendar } from '@hareui/vue'const datesWithEvents = [3, 7, 12, 15, 21, 28]function indicator(date: DateValue) {  return isToday(date, getLocalTimeZone()) || datesWithEvents.includes(date.day)}</script><template>  <Calendar aria-label="Event date" :indicator="indicator" /></template>

Custom Navigation Icons

Use the #navIcon slot to replace the default chevron icons.

October 2026
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
Event date, October 2026
<script setup lang="ts">import { Calendar } from '@hareui/vue'</script><template>  <Calendar aria-label="Event date">    <template #navIcon="{ direction }">      <span class="text-xs font-bold">{{ direction === 'previous' ? '«' : '»' }}</span>    </template>  </Calendar></template>

Real-World Example

October 2026
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
Booking date, October 2026
Has bookings Weekend/Unavailable
<script setup lang="ts">import type { CalendarDate, DateValue } from '@internationalized/date'import { Button, Calendar } from '@hareui/vue'import { getLocalTimeZone, isWeekend, today } from '@internationalized/date'import { computed, shallowRef } from 'vue'const selectedDate = shallowRef<CalendarDate | null>(null)const bookedDates = [5, 6, 12, 13, 14, 20]function isDateUnavailable(date: DateValue) {  return isWeekend(date, 'en-US') || bookedDates.includes(date.day)}function indicator(date: DateValue) {  return !isWeekend(date, 'en-US') && bookedDates.includes(date.day)}const minValue = computed(() => today(getLocalTimeZone()))</script><template>  <div class="flex flex-col items-center gap-4">    <Calendar      aria-label="Booking date"      v-model="selectedDate"      :is-date-unavailable="isDateUnavailable"      :min-value="minValue"      :indicator="indicator"    />    <div class="flex flex-col gap-2 text-center">      <div class="flex items-center justify-center gap-4 text-xs text-muted">        <span class="flex items-center gap-1">          <span class="size-2 rounded-full bg-muted" /> Has bookings        </span>        <span class="flex items-center gap-1">          <span class="size-2 rounded-full bg-default" /> Weekend/Unavailable        </span>      </div>      <Button v-if="selectedDate" size="sm" variant="primary">        Book {{ selectedDate.toString() }}      </Button>    </div>  </div></template>

International Calendars

By default, Calendar displays dates using the calendar system for the given locale. The example below shows the Indian calendar system.

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
Event date, शक 1948 अश्विन
<script setup lang="ts">import type { CalendarIdentifier } from '@internationalized/date'import { createCalendar, getLocalTimeZone, today, toCalendar } from '@internationalized/date'import { Calendar } from '@hareui/vue'const locale = 'hi-IN-u-ca-indian'const calendarId = new Intl.DateTimeFormat(locale).resolvedOptions().calendar as CalendarIdentifierconst defaultValue = toCalendar(today(getLocalTimeZone()), createCalendar(calendarId))</script><template>  <Calendar aria-label="Event date" :locale="locale" :default-value="defaultValue" year-picker /></template>

Note: update:modelValue always emits a date in the same calendar system as model-value or default-value (Gregorian if neither is given), regardless of the displayed locale.

Multiple Months

Set number-of-months to show several months side by side. The previous/next buttons always page by the full visible width, matching React Aria's default behavior for multi-month calendars.

October 2026
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
November 2026
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
Trip dates, October - November 2026
<script setup lang="ts">import { Calendar } from '@hareui/vue'</script><template>  <Calendar aria-label="Trip dates" class="@container-normal w-full max-w-none overflow-x-auto" :number-of-months="2" /></template>

Customization

Tailwind CSS

October 2026
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
Custom styled calendar, October 2026
<script setup lang="ts">import { Calendar } from '@hareui/vue'</script><template>  <Calendar    aria-label="Custom styled calendar"    class="w-63 rounded-2xl border border-border/80 bg-surface p-3 shadow-sm ring-1 ring-accent/5 dark:border-border/90 dark:ring-accent/10"  /></template>

Global Configuration

To customize the Calendar classes for the whole app, extend the calendar theme in createHareUI:

app.use(createHareUI({
  ui: {
    calendar: {
      slots: {
        base: 'rounded-2xl border border-border bg-surface p-3 shadow-sm',
        cell: 'rounded-lg',
      },
    },
  },
}))

Styling Reference

Interactive States

Calendar sets these data attributes (from Reka UI):

  • Selected: [data-selected] on [data-slot="calendar-cell"]
  • Today: [data-today]
  • Unavailable: [data-unavailable]
  • Outside the visible month: [data-outside-view]
  • Hover: :hover
  • Press: :active
  • Focus visible: :focus-visible
  • Disabled: :disabled or [data-disabled]

Reka UI Limitations

  • Week and day views (visible-duration) are rendered by HareUI's own grid, since Reka's CalendarRoot/RangeCalendarRoot always build full month grids. They follow React Aria's visibleDuration behaviour (alignment, paging, page-behavior, keyboard navigation) and HeroUI's --week-view/--day-view styles; the year picker, number-of-months and fixed-weeks only apply to the month view.
  • fixed-weeks is boolean only (always 6 weeks). HeroUI's React weeksInMonth accepts any count up to 6; Reka's CalendarRoot only exposes the all-or-nothing fixedWeeks flag.

API Reference

Props

PropTypeDefaultDescription
ariaLabelstring-Accessible label for the calendar.
modelValueDateValue | DateValue[] | nullundefinedThe selected date(s). Use v-model to control it.
defaultValueDateValue | DateValue[]-The selected date(s) when initially rendered (uncontrolled). Reka UI's defaultValue prop type only accepts a single DateValue, not an array, even in multiple-selection mode (see reka-limitations in the Calendar report); cast at the call site.
placeholderDateValueundefinedThe currently displayed month/year. Use v-model:placeholder to control it.
defaultPlaceholderDateValue-The date initially displayed, before navigation (uncontrolled).
minValueDateValue1900-01-01The minimum selectable date.
maxValueDateValue2099-12-31The maximum selectable date.
isDateUnavailable((date: DateValue) => boolean)-Callback returning whether a given date is unavailable for selection.
disabledbooleanfalseWhether the whole calendar is disabled.
readonlybooleanfalseWhether the calendar is read-only (dates stay visible but cannot be changed).
localestring-The locale used for formatting and week calculations.
firstDayOfWeek"sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat"-The day the week starts on.
fixedWeeksbooleanfalseWhether to always display 6 weeks, avoiding layout shift between months. Reka UI only supports a fixed 6-week grid (HeroUI's React weeksInMonth accepts any count up to 6; see reka-limitations in the Calendar report).
selectionMode"multiple" | "single""single"Whether one or multiple dates can be selected.
yearPickerbooleanfalseReplaces the plain month/year heading with a toggleable year grid.
indicator((date: DateValue) => boolean)-Callback returning whether a small dot indicator should render under a date.
pagedNavigationbooleanfalseThis property causes the previous/next buttons to navigate by the number of months displayed at once rather than one month.
numberOfMonthsnumber1Shows this many months side by side. Navigating with the prev/next buttons always pages by the full visible width (Reka's pagedNavigation, forced on regardless of the pagedNavigation prop, matching React Aria's default pageBehavior: "visible"). Reka's heading only formats the combined range, so each panel renders its own "<Month> <Year>" label derived from that panel's Grid.value (see reka-limitations in the Calendar report).
invalidbooleanfalseMarks the calendar as invalid for consumer-driven validation. Reka's own isInvalid is a read-only computed derived from isDateUnavailable/minValue/maxValue, with no input prop to override it (see reka-limitations in the Calendar report), so this sets aria-invalid/data-invalid directly instead. HeroUI's styles don't key off it either, so it has no visual effect by itself.
dir"ltr" | "rtl"-The reading direction of the calendar.
visibleDurationVisibleDuration{ months: 1 }The visible time range. { weeks: n } shows a week view and { days: n } a day view (a rolling window of consecutive days); anything else shows the month view (use number-of-months for several months). Navigation advances by the visible range.
pageBehaviorPageBehavior"visible"In week/day views, whether the prev/next buttons and page keys move by the whole visible range ('visible') or by one week/day ('single').
uiComponentSlots<{ slots: { base: string; panels: string; panel: string; header: string[]; heading: string; navButton: string[]; navButtonIcon: string; grid: string[]; gridHeader: string; gridBody: string; gridRow: string; headerCell: string; cell: string[]; cellIndicator: string; yearPickerTrigger: string[]; yearPickerTriggerHeading: string[]; yearPickerTriggerIndicator: string[]; yearPickerGrid: string[]; yearPickerYearCell: string[]; }; variants: { view: { month: {}; week: { cell: string; }; day: { cell: string; grid: string; gridHeader: string; gridBody: string; }; }; }; defaultVariants: { view: string; }; }>-Per-slot class overrides.

Slots

SlotPropsDescription
navIcon{ direction: "previous" | "next"; }Replaces the chevron icon inside a nav button.
cell{ date: DateValue; today: boolean; selected: boolean; disabled: boolean; unavailable: boolean; outsideView: boolean; }Replaces a cell's default formatted-date content.

Emits

EventPayloadDescription
update:modelValue[value: DateValue | DateValue[] | undefined]-
update:placeholder[value: DateValue]-

On this page

No Headings