Skip to content

RangeCalendar ​

A range calendar picks two dates that belong to each other — a stay, a reporting period. Every part mirrors the Calendar's under its own name, because the grid is the same grid; what differs is that the selection has two ends and the days between them are shown as part of it.

ts
import {
  RangeCalendar,
  RangeCalendarCell,
  RangeCalendarGrid,
  RangeCalendarGridBody,
  RangeCalendarGridHeader,
  RangeCalendarHeader,
  RangeCalendarHeading,
  RangeCalendarNavButton,
} from "ropav";

Stay, 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-basic.vue
vue
<script setup lang="ts">
import {
  RangeCalendar,
  RangeCalendarCell,
  RangeCalendarGrid,
  RangeCalendarGridBody,
  RangeCalendarGridHeader,
  RangeCalendarHeader,
  RangeCalendarHeaderCell,
  RangeCalendarHeading,
  RangeCalendarNavButton,
} from "ropav";
</script>

<template>
  <RangeCalendar aria-label="Stay">
    <RangeCalendarHeader>
      <RangeCalendarNavButton slot="previous" />
      <RangeCalendarHeading />
      <RangeCalendarNavButton slot="next" />
    </RangeCalendarHeader>

    <RangeCalendarGrid>
      <RangeCalendarGridHeader>
        <template #default="{ day }">
          <RangeCalendarHeaderCell>{{ day }}</RangeCalendarHeaderCell>
        </template>
      </RangeCalendarGridHeader>
      <RangeCalendarGridBody>
        <template #default="{ date }">
          <RangeCalendarCell :date />
        </template>
      </RangeCalendarGridBody>
    </RangeCalendarGrid>
  </RangeCalendar>
</template>

The first press sets the start, the second the end, and hovering between the two previews the range. Pressing again starts over rather than extending, which is what stops a reader who mis-clicked from having to undo in a particular order.

Bounds and gaps ​

min-value and max-value bound the whole range, and is-date-unavailable rules out days inside it. A range cannot span an unavailable day: the end is capped before it, so the reader cannot select over a blackout period and only find out on submit.

visible-duration showing two months at once is the usual choice here — a range that crosses a month boundary is hard to judge one month at a time.

Accessibility ​

  • The two ends are announced as a range rather than as two separate dates, so a reader is told what they have selected and not merely where the cursor is.
  • Name the calendar. "Stay" or "Reporting period" tells a reader what the two ends mean; "Range" tells them what they already know.
  • The days between the ends are shown as part of the selection and announced that way, which is what makes a range readable without seeing the highlight across the grid.

API ​

RangeCalendar

PropTypeDefaultDescription
allowsNonContiguousRangesboolean—Whether a range may span an unavailable date.
ariaDescribedbystring—
ariaDetailsstring—
ariaLabelstring—
ariaLabelledbystring—
autoFocusboolean—Whether the calendar takes focus as soon as it mounts.
commitBehavior"clear" | "reset" | "select""select"What to do with a half-built range when the pointer or focus leaves.
createCalendar((identifier: CalendarIdentifier) => Calendar)—Builds the calendar system for an identifier. Injectable so a build can ship fewer of them.
defaultFocusedValueDateValue | null—
defaultValueDateRange | null—
defaultYearPickerOpenbooleanfalse
firstDayOfWeek"fri" | "mon" | "sat" | "sun" | "thu" | "tue" | "wed"—The day a week starts on, when it should not follow the locale.
focusedValueDateValue | null—
idstring—
isDateUnavailable((date: DateValue, anchorDate: CalendarDate | null) => boolean)—Rules a date out even though it is inside the range. The end the user pinned first comes along, so what is available can depend on where the range started.
isDisabledboolean—
isInvalidboolean—
isReadOnlyboolean—
isYearPickerOpenboolean—Whether the year picker is open.
maxValueDateValue | null—
minValueDateValue | null—
pageBehavior"single" | "visible""visible"
selectionAlignment"center" | "end" | "start""center"
valueDateRange | null—
visibleDurationDateDuration{months: 1}How much is on screen at once, which is also what paging moves by.
weeksInMonthnumber—A fixed number of week rows, so a month grid does not change height while paging.

The default slot receives:

Slot propType
isDisabledboolean
isInvalidboolean
stateAnyCalendarState
EventPayload
update:focusedValueCalendarDate
update:valueDateRange | null
update:yearPickerOpenboolean

RangeCalendarCell

PropTypeDefaultDescription
date*CalendarDate—The date this cell stands for.

The default slot receives:

Slot propType
dateCalendarDate
formattedDatestring
isDisabledboolean
isFocusedboolean
isFocusVisibleboolean
isHoveredboolean
isInvalidboolean
isOutsideMonthboolean
isOutsideVisibleRangeboolean
isPressedboolean
isSelectedboolean
isSelectionEndboolean
isSelectionStartboolean
isTodayboolean
isUnavailableboolean

RangeCalendarCellIndicator

Slots: default.

RangeCalendarGrid

PropTypeDefaultDescription
offsetDateDuration—How far past the first visible date this grid starts, for a multi-month calendar.
weekdayStyle"long" | "narrow" | "short""short"How wide the weekday names above the grid are written.

Slots: default.

RangeCalendarGridBody

The default slot receives:

Slot propType
dateCalendarDate

RangeCalendarGridHeader

The default slot receives:

Slot propType
daystring

RangeCalendarHeader

Slots: default.

RangeCalendarHeaderCell

Slots: default.

RangeCalendarHeading

PropTypeDefaultDescription
formatCalendarHeadingFormatOptions—Overrides the parts the heading is written from.
offsetDateDuration—How far past the first visible date this heading describes.

RangeCalendarNavButton

PropTypeDefaultDescription
slot"next" | "previous""next"Which direction this button pages.

Slots: default.