RangeCalendar

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

Usage

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

Anatomy

RangeCalendar 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. Unlike Calendar, each day is a cell wrapping a cellButton — Reka puts the selection/today/outside-view state on the inner button, not the outer cell.

<template>
  <RangeCalendar aria-label="Trip dates">
    <template #navIcon="{ direction }" />                                  <!-- replaces both chevrons -->
    <template #cell="{ date, today, selected, disabled, unavailable, outsideView, selectionStart, selectionEnd }" /> <!-- replaces formatted day -->
  </RangeCalendar>
</template>
ui keydata-slotElement
baserange-calendarroot element
headerrange-calendar-headerrow containing the heading/year-picker trigger and nav buttons
headingcalendar-headingformatted month/year label (hidden when year-picker)
navButtonrange-calendar-nav-buttonprevious/next button
navButtonIconcalendar-nav-button-icondefault chevron inside a nav button
gridrange-calendar-gridthe day grid
gridHeaderrange-calendar-grid-headerweekday label row wrapper
gridBodyrange-calendar-grid-bodyweek rows wrapper
gridRowrange-calendar-grid-rowone week row
headerCellcalendar-header-cellweekday label cell
cellrange-calendar-cellthe interactive day cell — carries all state attributes
cellButtonrange-calendar-cell-buttondecorative inner circle (visual only, no attributes)
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

Trip dates, February 2025
February 2025
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
1
2
<script setup lang="ts">import { RangeCalendar } from '@hareui/vue'import { parseDate } from '@internationalized/date'</script><template>  <RangeCalendar aria-label="Trip dates" :default-value="{ start: parseDate('2025-02-03'), end: parseDate('2025-02-12') }" first-day-of-week="mon" /></template>

Disabled

Trip dates, October 2026
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
Range calendar is disabled
<script setup lang="ts">import { Description, RangeCalendar } from '@hareui/vue'</script><template>  <div class="flex flex-col items-center gap-4">    <RangeCalendar aria-label="Trip dates" disabled />    <Description class="text-center">      Range calendar is disabled    </Description>  </div></template>

Read Only

Trip dates, October 2026
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
Range calendar is read-only
<script setup lang="ts">import { Description, RangeCalendar } from '@hareui/vue'import { getLocalTimeZone, today } from '@internationalized/date'const now = today(getLocalTimeZone())</script><template>  <div class="flex flex-col items-center gap-4">    <RangeCalendar aria-label="Trip dates" :default-value="{ start: now, end: now.add({ days: 4 }) }" readonly />    <Description class="text-center">      Range 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).

Trip 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
<script setup lang="ts">import { RangeCalendar } from '@hareui/vue'</script><template>  <RangeCalendar aria-label="Trip dates" year-picker /></template>

Controlled

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

Trip dates, December 2025
December 2025
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
Selected range: (none)
<script setup lang="ts">import type { DateRange } from '@hareui/vue'import type { DateValue } from '@internationalized/date'import { Button, ButtonGroup, Description, RangeCalendar } from '@hareui/vue'import { getLocalTimeZone, parseDate, startOfMonth, startOfWeek, today } from '@internationalized/date'import { shallowRef } from 'vue'const value = shallowRef<DateRange | undefined>()const focusedDate = shallowRef<DateValue>(parseDate('2025-12-25'))</script><template>  <div class="flex flex-col items-center gap-4">    <ButtonGroup variant="tertiary">      <Button @click="focusedDate = today(getLocalTimeZone())">        This week      </Button>      <Button @click="focusedDate = startOfWeek(today(getLocalTimeZone()).add({ weeks: 1 }), 'en-US')">        Next week      </Button>      <Button @click="focusedDate = startOfMonth(today(getLocalTimeZone()).add({ months: 1 }))">        Next month      </Button>    </ButtonGroup>    <RangeCalendar aria-label="Trip dates" first-day-of-week="mon" v-model="value" v-model:placeholder="focusedDate" />    <Description class="text-center">      Selected range: {{ value?.start && value?.end ? `${value.start.toString()} -> ${value.end.toString()}` : '(none)' }}    </Description>    <div class="flex gap-2">      <Button        size="sm"        variant="secondary"        @click="() => {          const start = today(getLocalTimeZone())          value = { start, end: start.add({ days: 6 }) }          focusedDate = start        }"      >        Set 1 week      </Button>      <Button        size="sm"        variant="secondary"        @click="() => {          const start = parseDate('2025-12-20')          value = { start, end: parseDate('2025-12-31') }          focusedDate = start        }"      >        Set Holidays      </Button>      <Button size="sm" variant="tertiary" @click="value = undefined">        Clear      </Button>    </div>  </div></template>

Min and Max Dates

Trip dates, October 2026
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
Select dates between today and 2027-01-03
<script setup lang="ts">import { Description, RangeCalendar } 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">    <RangeCalendar aria-label="Trip dates" :min-value="minDate" :max-value="maxDate" />    <Description class="text-center">      Select dates between today and {{ maxDate.toString() }}    </Description>  </div></template>

Unavailable Dates

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

Trip dates, October 2026
October 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
Some days are unavailable
<script setup lang="ts">import type { DateValue } from '@internationalized/date'import { Description, RangeCalendar } from '@hareui/vue'import { getLocalTimeZone, today } from '@internationalized/date'const now = today(getLocalTimeZone())const blockedRanges = [  [now.add({ days: 2 }), now.add({ days: 5 })],  [now.add({ days: 12 }), now.add({ days: 13 })],] as constfunction isDateUnavailable(date: DateValue) {  return blockedRanges.some(([start, end]) => date.compare(start) >= 0 && date.compare(end) <= 0)}</script><template>  <div class="flex flex-col items-center gap-4">    <RangeCalendar      aria-label="Trip dates"      :default-value="{ start: now.add({ days: 6 }), end: now.add({ days: 9 }) }"      first-day-of-week="mon"      :is-date-unavailable="isDateUnavailable"    />    <Description class="text-center">      Some days are unavailable    </Description>  </div></template>

Anchor-Based Unavailable Dates

When selecting a range, is-date-unavailable receives a second argument, anchorDate, set to the first selected date. Use it to limit which end dates are valid (for example, within 7 days of the start).

Trip dates, October 2026
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
After selecting a start date, only dates within 7 days are available
<script setup lang="ts">import type { DateValue } from '@internationalized/date'import { Description, RangeCalendar } from '@hareui/vue'import { getLocalTimeZone, today } from '@internationalized/date'const now = today(getLocalTimeZone())function isDateUnavailable(date: DateValue, anchorDate: DateValue | null) {  return anchorDate != null && Math.abs(date.compare(anchorDate)) > 7}</script><template>  <div class="flex flex-col items-center gap-4">    <RangeCalendar aria-label="Trip dates" :is-date-unavailable="isDateUnavailable" :min-value="now" />    <Description class="text-center">      After selecting a start date, only dates within 7 days are available    </Description>  </div></template>

Non-Contiguous Ranges

Set allow-non-contiguous-ranges to let a selection skip over unavailable dates in the middle of the range instead of resetting the selection.

Trip dates, October 2026
October 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
Non-contiguous ranges are allowed across unavailable dates
<script setup lang="ts">import type { DateValue } from '@internationalized/date'import { Description, RangeCalendar } from '@hareui/vue'import { getLocalTimeZone, today } from '@internationalized/date'const now = today(getLocalTimeZone())const blockedRanges = [  [now.add({ days: 2 }), now.add({ days: 5 })],  [now.add({ days: 12 }), now.add({ days: 13 })],] as constfunction isDateUnavailable(date: DateValue) {  return blockedRanges.some(([start, end]) => date.compare(start) >= 0 && date.compare(end) <= 0)}</script><template>  <div class="flex flex-col items-center gap-4">    <RangeCalendar      aria-label="Trip dates"      allow-non-contiguous-ranges      :default-value="{ start: now.add({ days: 1 }), end: now.add({ days: 9 }) }"      first-day-of-week="mon"      :is-date-unavailable="isDateUnavailable"    />    <Description class="text-center">      Non-contiguous ranges are allowed across unavailable dates    </Description>  </div></template>

Weeks in Month

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

Trip dates, October 2026
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
Always shows 6 weeks per month to avoid layout shift when navigating
<script setup lang="ts">import { Description, RangeCalendar } from '@hareui/vue'</script><template>  <div class="flex flex-col items-center gap-4">    <RangeCalendar aria-label="Trip dates" 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 { RangeCalendar, 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))"    />    <RangeCalendar :key="weeks" aria-label="Trip dates" :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 { RangeCalendar, 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))"    />    <RangeCalendar :key="days" aria-label="Trip dates" :visible-duration="{ days }" />  </div></template>

Focused Value

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

Trip dates, June 2025
June 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
29
30
1
2
3
4
5
6
Focused: 2025-06-15
<script setup lang="ts">import type { DateValue } from '@internationalized/date'import { Button, Description, RangeCalendar } 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">    <RangeCalendar aria-label="Trip dates" first-day-of-week="mon" 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.

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

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.

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

Invalid

Pass invalid to mark the calendar as invalid for consumer-driven validation (e.g. a selected range that's too long).

Trip dates, October 2026
October 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

Maximum stay duration is 1 week

<script setup lang="ts">import type { DateRange } from '@hareui/vue'import { Description, RangeCalendar } from '@hareui/vue'import { getLocalTimeZone, today } from '@internationalized/date'import { computed, shallowRef } from 'vue'const now = today(getLocalTimeZone())const value = shallowRef<DateRange>({ start: now.add({ days: 6 }), end: now.add({ days: 14 }) })const isInvalid = computed(() => !!value.value.end && !!value.value.start && value.value.end.compare(value.value.start) > 7)</script><template>  <div class="flex flex-col items-center gap-4">    <RangeCalendar aria-label="Trip dates" v-model="value" first-day-of-week="mon" :invalid="isInvalid" />    <p v-if="isInvalid" class="text-sm text-danger">      Maximum stay duration is 1 week    </p>    <Description v-else class="text-center">      Select a stay of up to 7 days    </Description>  </div></template>

Real-World Example

Booking range, October 2026
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
Has bookings Weekend/Unavailable
<script setup lang="ts">import type { DateRange } from '@hareui/vue'import type { DateValue } from '@internationalized/date'import { Button, RangeCalendar } from '@hareui/vue'import { getLocalTimeZone, isWeekend, today } from '@internationalized/date'import { computed, shallowRef } from 'vue'const selectedRange = shallowRef<DateRange | undefined>()const blockedDates = [5, 6, 12, 13, 14, 20]function isDateUnavailable(date: DateValue) {  return isWeekend(date, 'en-US') || blockedDates.includes(date.day)}function indicator(date: DateValue) {  return !isWeekend(date, 'en-US') && blockedDates.includes(date.day)}const minValue = computed(() => today(getLocalTimeZone()))</script><template>  <div class="flex flex-col items-center gap-4">    <RangeCalendar      aria-label="Booking range"      v-model="selectedRange"      :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="selectedRange?.start && selectedRange?.end" size="sm" variant="primary">        Book {{ selectedRange.start.toString() }} -&gt; {{ selectedRange.end.toString() }}      </Button>    </div>  </div></template>

International Calendars

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

Trip dates, अक्टूबर 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
<script setup lang="ts">import { RangeCalendar } from '@hareui/vue'</script><template>  <RangeCalendar aria-label="Trip dates" locale="hi-IN-u-ca-indian" year-picker /></template>

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

Customization

Tailwind CSS

Hotel stay, February 2025
February 2025
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
1
2
<script setup lang="ts">import { RangeCalendar } from '@hareui/vue'import { parseDate } from '@internationalized/date'const cellButtonClass = [  'rounded-md',  'data-[outside-view]:text-muted data-[outside-view]:opacity-50',  'hover:not-data-[selected]:bg-default',  'data-[today]:bg-success-soft data-[today]:text-success-soft-foreground',  'data-[today]:hover:not-data-[selected]:bg-success-soft-hover',  'data-[selected=true]:rounded-none data-[selected=true]:bg-success-soft',  'data-[outside-view]:data-[selected=true]:bg-default/20',  'data-[selection-start=true]:rounded-tl-md data-[selection-start=true]:rounded-bl-md',  'data-[selection-end=true]:rounded-tr-md data-[selection-end=true]:rounded-br-md',  'data-[selection-start=true]:bg-success data-[selection-start=true]:text-success-foreground',  'data-[selection-start=true]:active:bg-success-hover',  'data-[selection-end=true]:bg-success data-[selection-end=true]:text-success-foreground',  'data-[selection-end=true]:active:bg-success-hover',].join(' ')</script><template>  <RangeCalendar    aria-label="Hotel stay"    class="w-63 rounded-xl border border-border/80 bg-surface p-3 shadow-sm ring-1 ring-success/10"    :default-value="{ start: parseDate('2025-02-08'), end: parseDate('2025-02-14') }"    first-day-of-week="mon"    :ui="{      header: 'px-0.5 pb-4',      heading: 'text-sm font-medium text-foreground',      navButton: 'rounded-md text-success hover:bg-success-soft active:scale-95',      headerCell: 'pb-2 text-xs font-medium text-muted',      cell: 'rounded-md',      cellButton: cellButtonClass,    }"  /></template>

Global Configuration

To customize the RangeCalendar classes for the whole app, extend the rangeCalendar theme in createHareUI:

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

Styling Reference

Interactive States

RangeCalendar sets these data attributes (from Reka UI), on [data-slot="range-calendar-cell"]:

  • Selected: [data-selected]
  • Selection start / end: [data-selection-start] / [data-selection-end]
  • 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.
  • Anchor-aware is-date-unavailable: Reka's matcher takes one argument, so RangeCalendar wraps the two-argument callback in a closure that reads the in-progress anchor (the picked start while end is unset, null once the range is complete, as in react-stately). It also narrows Reka's min-value/max-value to the run of available days around the anchor (react-stately's getAvailableRange), so paging past it is disabled, and marks the lone anchor cell as both selection start and end, as React Aria does.
  • fixed-weeks is boolean only (always 6 weeks). HeroUI's React weeksInMonth accepts any count up to 6; Reka's RangeCalendarRoot only exposes the all-or-nothing fixedWeeks flag.
  • invalid sets aria-invalid/data-invalid directly rather than through Reka: Reka's own isInvalid is a read-only computed derived from isDateUnavailable/minValue/maxValue, with no input prop to override it for arbitrary consumer validation (e.g. "stay too long").
  • No press-and-drag range selection: React Aria begins the live anchor on raw pointerdown (useCalendarState's anchorDate), letting a single press-move-release gesture preview and commit a range. Reka's RangeCalendarCellTrigger commits exclusively through its own click handler's changeDate state machine (RangeCalendarCellTrigger.vue:106-171), which is gated on startValue/lastPressedDateValue set by that same handler. Writing to Reka's internal startValue ref from an external pointerdown listener to emulate the anchor (tested against both a two-click gesture and a pointerdown+pointerenter+pointerup drag gesture) breaks the state machine rather than extending it: the click gesture commits nothing at all (changeDate takes the line 112-126 "already anchored" branch on the very first real click, since lastPressedDateValue doesn't match yet, so it returns without setting start or end), and the drag gesture commits only start (pointerup isn't one of Reka's commit triggers). There's no props-level override for the anchor/commit logic, so HeroUI's drag-select input method has no port; the two-click gesture (click a start day, hover to preview, click an end day) is fully supported and is what the demos above use.

API Reference

Props

PropTypeDefaultDescription
ariaLabelstring-Accessible label for the calendar.
modelValueDateRangeundefinedThe selected date range. Use v-model to control it.
defaultValueDateRange-The selected date range when initially rendered (uncontrolled).
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, anchorDate: DateValue | null) => boolean)-Callback returning whether a given date is unavailable for selection. During an in-progress range selection, anchorDate is the already-picked start date (or null before any date is picked), matching React Aria's isDateUnavailable(date, anchorDate).
disabledbooleanfalseWhether the whole calendar is disabled.
readonlybooleanfalseWhether the calendar is read-only (dates stay visible but cannot be changed).
allowNonContiguousRangesbooleanfalseWhether non-contiguous ranges are allowed, skipping over unavailable dates in the middle of the range instead of resetting the selection.
maximumDaysnumber-Restricts a range selection to a fixed number of days from the start date.
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 RangeCalendar report).
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 RangeCalendar report).
invalidbooleanfalseMarks the calendar as invalid for consumer-driven validation (e.g. a selected range that's too long). 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 RangeCalendar 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[]; cellButton: 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; selectionStart: boolean; selectionEnd: boolean; }Replaces a cell's default formatted-date content.

Emits

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

On this page

No Headings