Range Calendar
Accessible, unstyled two-click date range calendar that reuses the Calendar grid parts.
|
|
| Package |
@vizejs/ui/range-calendar |
| Maturity |
stable |
| Own the source |
vize lib pull range-calendar |
| Requires |
calendar, context, controllable-state, id, locale |
| Aliases |
date range calendar, booking calendar, two-click range |
| Covers |
React Aria RangeCalendar, Reka UI RangeCalendar, WAI-ARIA APG grid |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { RangeCalendar, RangeCalendarRoot, RangeCalendarGrid, RangeCalendarHeading, RangeCalendarMonthSelect, RangeCalendarNext, RangeCalendarPrev, RangeCalendarYearSelect } from "@vizejs/ui/range-calendar";
Or copy the source into your project with vize lib pull range-calendar (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Controlled two-click date range calendar with a fixed, SSR-safe "today". -->
<script setup lang="ts">
import { ref } from "vue";
import { RangeCalendar } from "@vizejs/ui/range-calendar";
import type { DateRange, PlainDate } from "@vizejs/ui/range-calendar";
const today: PlainDate = { year: 2026, month: 9, day: 25 };
const stay = ref<DateRange | null>({
start: { year: 2026, month: 9, day: 8 },
end: { year: 2026, month: 9, day: 11 },
});
function format(date: PlainDate): string {
return `${date.year}-${String(date.month).padStart(2, "0")}-${String(date.day).padStart(2, "0")}`;
}
</script>
<template>
<div>
<RangeCalendar
v-model="stay"
:today
locale="en-US"
aria-label="Hotel stay"
start-name="check-in"
end-name="check-out"
/>
<output>{{
stay ? `${format(stay.start)} → ${format(stay.end)}` : "No dates selected"
}}</output>
</div>
</template>
API
RangeCalendar
Source: src/families/date-time/range-calendar/range-calendar-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null | undefined |
undefined |
Consumer-owned base id; nullish values use a deterministic fallback. |
modelValue |
DateRange | null | undefined |
undefined |
Controlled range; undefined selects uncontrolled mode and null clears. |
defaultValue |
DateRange | null | undefined |
null |
Initial uncontrolled range. |
allowNonContiguousRanges |
boolean | undefined |
false |
Allow committed ranges to span unavailable dates. |
focusedDate |
PlainDate | null | undefined |
undefined |
Controlled keyboard focus date; also decides which months are visible. |
min |
PlainDate | null | undefined |
undefined |
Earliest selectable date, inclusive. |
max |
PlainDate | null | undefined |
undefined |
Latest selectable date, inclusive. |
isDateUnavailable |
DateMatcher | undefined |
undefined |
Predicate for dates that stay focusable but cannot be selected. |
locale |
string | undefined |
undefined |
BCP 47 locale; defaults to the nearest LocaleProvider. |
calendar |
string | undefined |
undefined |
Intl calendar used for display labels only, for example japanese. |
numberingSystem |
string | undefined |
undefined |
Intl numbering system for labels. |
dir |
CalendarDirection | undefined |
undefined |
Text direction; defaults to the nearest LocaleProvider. |
weekStartsOn |
Weekday | undefined |
undefined |
First day of week (0 = Sunday); defaults to the locale preference. |
weekdayFormat |
CalendarWeekdayFormat | undefined |
"short" |
Weekday column label width. |
numberOfMonths |
number | undefined |
1 |
Number of consecutive months rendered. |
pagedNavigation |
boolean | undefined |
false |
Move month controls by numberOfMonths instead of one month. |
fixedWeeks |
boolean | undefined |
false |
Always render six week rows per month. |
today |
PlainDate | null | undefined |
undefined |
Explicit current date; the SSR-safe way to mark today. |
now |
DateTimeNow | undefined |
undefined |
Injectable clock evaluated during setup on server and client. |
timeZone |
string | undefined |
undefined |
IANA time zone used with now and the post-mount host clock. |
disabled |
boolean | undefined |
false |
Disable navigation, focus, and selection. |
readOnly |
boolean | undefined |
false |
Allow navigation while blocking selection. |
startName |
string | undefined |
undefined |
Hidden input name that submits the ISO start date with forms. |
endName |
string | undefined |
undefined |
Hidden input name that submits the ISO end date with forms. |
ariaLabel |
string | undefined |
undefined |
Accessible name for the calendar group. |
ariaLabelledby |
string | undefined |
undefined |
Ids that label the calendar group; defaults to the heading. |
ariaDescribedby |
string | undefined |
undefined |
Ids that describe the calendar group. |
Events
| Event |
Payload |
Description |
update:modelValue |
[value: DateRange | null] |
Fired when the calendar requests a new controlled range. |
change |
[value: DateRange | null, previous: DateRange | null, nativeEvent: Event | null] |
Fired after a distinct committed range with the previous range and triggering event. |
select |
[value: DateRange, nativeEvent: Event] |
Fired for every completed range selection, even when the range is unchanged. |
anchor-change |
[anchor: PlainDate | null] |
Fired when the first endpoint of a new range is picked, or null when cancelled or completed. |
update:focusedDate |
[date: PlainDate] |
Fired when keyboard or navigation moves the focus date. |
Slots
| Slot |
Slot props |
Description |
default |
CalendarSlotState |
Range calendar composition. Receives months, weekdays, heading, and state; defaults to a header plus one grid per month. |
Exposed
| Member |
Type |
Description |
value |
— |
|
anchor |
— |
|
setValue |
— |
|
cancel |
— |
|
RangeCalendarRoot
Source: src/families/date-time/range-calendar/range-calendar-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null | undefined |
undefined |
Consumer-owned base id; nullish values use a deterministic fallback. |
modelValue |
DateRange | null | undefined |
undefined |
Controlled range; undefined selects uncontrolled mode and null clears. |
defaultValue |
DateRange | null | undefined |
null |
Initial uncontrolled range. |
allowNonContiguousRanges |
boolean | undefined |
false |
Allow committed ranges to span unavailable dates. |
focusedDate |
PlainDate | null | undefined |
undefined |
Controlled keyboard focus date; also decides which months are visible. |
min |
PlainDate | null | undefined |
undefined |
Earliest selectable date, inclusive. |
max |
PlainDate | null | undefined |
undefined |
Latest selectable date, inclusive. |
isDateUnavailable |
DateMatcher | undefined |
undefined |
Predicate for dates that stay focusable but cannot be selected. |
locale |
string | undefined |
undefined |
BCP 47 locale; defaults to the nearest LocaleProvider. |
calendar |
string | undefined |
undefined |
Intl calendar used for display labels only, for example japanese. |
numberingSystem |
string | undefined |
undefined |
Intl numbering system for labels. |
dir |
CalendarDirection | undefined |
undefined |
Text direction; defaults to the nearest LocaleProvider. |
weekStartsOn |
Weekday | undefined |
undefined |
First day of week (0 = Sunday); defaults to the locale preference. |
weekdayFormat |
CalendarWeekdayFormat | undefined |
"short" |
Weekday column label width. |
numberOfMonths |
number | undefined |
1 |
Number of consecutive months rendered. |
pagedNavigation |
boolean | undefined |
false |
Move month controls by numberOfMonths instead of one month. |
fixedWeeks |
boolean | undefined |
false |
Always render six week rows per month. |
today |
PlainDate | null | undefined |
undefined |
Explicit current date; the SSR-safe way to mark today. |
now |
DateTimeNow | undefined |
undefined |
Injectable clock evaluated during setup on server and client. |
timeZone |
string | undefined |
undefined |
IANA time zone used with now and the post-mount host clock. |
disabled |
boolean | undefined |
false |
Disable navigation, focus, and selection. |
readOnly |
boolean | undefined |
false |
Allow navigation while blocking selection. |
startName |
string | undefined |
undefined |
Hidden input name that submits the ISO start date with forms. |
endName |
string | undefined |
undefined |
Hidden input name that submits the ISO end date with forms. |
ariaLabel |
string | undefined |
undefined |
Accessible name for the calendar group. |
ariaLabelledby |
string | undefined |
undefined |
Ids that label the calendar group; defaults to the heading. |
ariaDescribedby |
string | undefined |
undefined |
Ids that describe the calendar group. |
Events
| Event |
Payload |
Description |
update:modelValue |
[value: DateRange | null] |
Fired when the calendar requests a new controlled range. |
change |
[value: DateRange | null, previous: DateRange | null, nativeEvent: Event | null] |
Fired after a distinct committed range with the previous range and triggering event. |
select |
[value: DateRange, nativeEvent: Event] |
Fired for every completed range selection, even when the range is unchanged. |
anchor-change |
[anchor: PlainDate | null] |
Fired when the first endpoint of a new range is picked, or null when cancelled or completed. |
update:focusedDate |
[date: PlainDate] |
Fired when keyboard or navigation moves the focus date. |
Slots
| Slot |
Slot props |
Description |
default |
CalendarSlotState |
Range calendar composition. Receives months, weekdays, heading, and state; defaults to a header plus one grid per month. |
Exposed
| Member |
Type |
Description |
mode |
CalendarSelectionMode |
Selection model of the root. |
months |
readonly CalendarMonthState[] |
Visible month grids; empty while pending. |
weekdays |
readonly CalendarWeekdayLabel[] |
Localized weekday labels in column order. |
heading |
string |
Localized heading for the visible months. |
focusedDate |
PlainDate | null |
Keyboard focus target, or null while pending. |
today |
PlainDate | null |
Resolved current date, or null before a clock is available. |
locale |
string |
Resolved BCP 47 locale. |
direction |
CalendarDirection |
Resolved text direction. |
weekStartsOn |
Weekday |
Resolved first day of week. |
disabled |
boolean |
Whether every control is disabled. |
readOnly |
boolean |
Whether selection is blocked while navigation works. |
pending |
boolean |
Whether no date is known yet: no value, focused date, today, or now was supplied, so the grid waits for the client clock after mount. |
state |
CalendarState |
Stable root state token. |
root |
HTMLDivElement | null |
Rendered root element. |
focus |
(options?: FocusOptions) => boolean |
Focus the roving day button; returns whether focus moved. |
setFocusedDate |
(date: PlainDate) => void |
Move the keyboard focus target (and visible months) to a date, clamped to min/max. |
setVisibleMonth |
(month: PlainYearMonth) => void |
Show a month as the first visible month. |
navigate |
(unit: CalendarNavigationUnit, direction: -1 | 1) => boolean |
Navigate by one page of months or years; returns whether the view moved. |
value |
DateRange | null |
Current committed range. |
anchor |
PlainDate | null |
First endpoint of a range that is still being picked, or null. |
setValue |
(value: DateRange | null) => boolean |
Request a new range; returns whether it differs. |
cancel |
() => boolean |
Cancel a pending anchor; returns whether one was pending. |
RangeCalendarGrid
Source: src/families/date-time/calendar/calendar-grid.vue
Props
| Prop |
Type |
Default |
Description |
monthIndex |
number |
0 |
Zero-based index of the visible month this grid renders. |
showWeekNumbers |
boolean |
false |
Render a leading row header with the ISO 8601 week number of each row. |
weekNumberLabel |
string |
"Wk" |
Visible week-number column header. |
weekNumberHeader |
string |
"Week" |
Full week-number column header used as abbr. |
Slots
| Slot |
Slot props |
Description |
weekday |
CalendarWeekdaySlotState |
Weekday column header content. Receives the weekday, its labels, and column index. |
weekNumber |
CalendarWeekNumberSlotState |
Week-number row header content. Receives the ISO week and the row's days. |
day |
CalendarDayState |
Day button content. Receives the complete day state; defaults to the localized day number. |
RangeCalendarHeading
Source: src/families/date-time/calendar/calendar-heading.vue
Slots
| Slot |
Slot props |
Description |
default |
CalendarHeadingSlotState |
Heading content. Receives the localized heading and visible months. |
RangeCalendarMonthSelect
Source: src/families/date-time/calendar/calendar-month-select.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
"Month" |
Accessible name of the native select. |
Events
| Event |
Payload |
Description |
change |
[month: number, nativeEvent: Event] |
Fired after the user picks a month, with the ISO month number. |
RangeCalendarNext
Source: src/families/date-time/calendar/calendar-next.vue
Props
| Prop |
Type |
Default |
Description |
unit |
CalendarNavigationUnit |
"month" |
Whether the control pages by month (or numberOfMonths when paged) or by year. |
ariaLabel |
string |
undefined |
Accessible name; defaults to "Next month" or "Next year". |
Events
| Event |
Payload |
Description |
click |
[nativeEvent: MouseEvent] |
Fired before navigation. Call preventDefault() to keep the view unchanged. |
Slots
| Slot |
Slot props |
Description |
default |
CalendarNavigationSlotState |
Control content. Receives the unit and whether navigation is possible. |
RangeCalendarPrev
Source: src/families/date-time/calendar/calendar-prev.vue
Props
| Prop |
Type |
Default |
Description |
unit |
CalendarNavigationUnit |
"month" |
Whether the control pages by month (or numberOfMonths when paged) or by year. |
ariaLabel |
string |
undefined |
Accessible name; defaults to "Previous month" or "Previous year". |
Events
| Event |
Payload |
Description |
click |
[nativeEvent: MouseEvent] |
Fired before navigation. Call preventDefault() to keep the view unchanged. |
Slots
| Slot |
Slot props |
Description |
default |
CalendarNavigationSlotState |
Control content. Receives the unit and whether navigation is possible. |
RangeCalendarYearSelect
Source: src/families/date-time/calendar/calendar-year-select.vue
Props
| Prop |
Type |
Default |
Description |
from |
number |
undefined |
First listed year; defaults to the min year or ten years before the view. |
to |
number |
undefined |
Last listed year; defaults to the max year or ten years after the view. |
ariaLabel |
string |
"Year" |
Accessible name of the native select. |
Events
| Event |
Payload |
Description |
change |
[year: number, nativeEvent: Event] |
Fired after the user picks a year. |
Behavior
Normative state x input -> outcome table for range-calendar-root.vue
(@vizejs/ui/range-calendar). The root publishes the same context as
CalendarRoot, so calendar-grid.vue, calendar-heading.vue,
calendar-prev.vue, calendar-next.vue, and the month/year selects are reused
as RangeCalendar* parts; keyboard, locale, bounds, and SSR rules from
calendar.behavior.md apply unchanged. Every row is proven by the named test.
| # |
State |
Input |
Outcome |
Proven by |
| R1 |
empty |
first activation |
the date becomes the anchor (data-anchor, anchor-change); grids are aria-multiselectable |
two activations anchor, preview, and commit an ordered range |
| R2 |
anchored |
pointer enter |
the preview range is marked with data-in-range, data-range-start, data-range-end, and data-preview; previewed middles are aria-selected="false" |
two activations anchor, preview, and commit an ordered range |
| R3 |
anchored |
second activation |
an ordered range commits: anchor-change(null), update:modelValue, change; middles become range-middle and aria-selected="true"; startName/endName inputs submit ISO |
two activations anchor, preview, and commit an ordered range |
| R4 |
anchored |
keyboard focus / Escape |
focus movement previews the range; Escape cancels the anchor and is consumed only when an anchor exists |
keyboard focus previews the range and Escape cancels the anchor |
| R5 |
unavailable dates |
completing activation |
a range spanning an unavailable date restarts the anchor unless allowNonContiguousRanges |
unavailable dates break ranges unless non-contiguous ranges are allowed |
| R6 |
controlled / read-only |
render / expose / activation |
controlled ranges render across months (outside days included); setValue, cancel, anchor are exposed; read-only blocks anchoring |
controlled ranges, read-only mode, and the exposed API |
| R7 |
SSR/hydration |
isolated requests |
byte-identical markup, no anchor or preview state, silent hydration |
renders byte-identical range markup across isolated SSR requests, hydrates a range calendar without mismatches |
Props and emits beyond CalendarRoot
| Surface |
Contract |
modelValue / defaultValue |
DateRange | null; ranges are normalized so start <= end. |
allowNonContiguousRanges |
Permit committed ranges that include unavailable dates. Default false. |
startName / endName |
Hidden inputs submitting the ISO endpoints. |
anchor-change(anchor) |
Fires when the first endpoint is picked, cancelled, or completed. |
select(range, event) |
Fires for every completed selection, even when unchanged. |
| root data |
data-mode="range", data-start, data-end, data-anchor. |