CalendarRange
Overview
CalendarRange displays one or more month grids and lets users pick a start and end date directly from the calendar surface. Selecting the first date starts the range, a live hover preview highlights the candidate range, and the second click commits it — with reversed selections sorted automatically. Use it for booking, scheduling, or any range-picking surface that needs a keyboard-navigable grid instead of two text inputs. Pair it with SDateRangePicker when you need a popup trigger, or use it standalone inside forms and custom layouts.
Usage
Features
- 📅 Month grids — one or more grids (
numberOfMonths) with locale-aware weekday headers, today, and outside-view markers - 🎯 Range picking — click start then end (in either order, sorted automatically); hover previews the candidate range before commit
- 🎚 Controlled / uncontrolled —
v-model/defaultValuebound to aDateRange({ start, end }), backed byuseControllableState - ⌨️ Full keyboard support —
ArrowLeft/ArrowRight/ArrowUp/ArrowDownnavigation with RTL reversal, cross-month paging, and disabled-date skipping;Enter/Spaceselects the start/end - ⛓ Range constraints —
allowNonContiguousRangestoggles gaps,maximumDayscaps the span,isDateDisabled/isDateUnavailable/isDateHighlightablecontrol every day,minValue/maxValueclamp the page - 📌 Fixed anchors —
fixedDate('start'/'end') keeps one end pinned while re-picking the other;preventDeselectkeeps a lone start sticky - 🧩 Composable slots —
day,head-cell,heading,prev, andnextslots with rich slot props;*Propspassthrough for every sub-part - 🔤 Localized UI —
locale,weekStartsOn, andweekdayFormat; month/year Select controls carry localizedaria-labels - 🚫 Disabled / readonly —
disabledblocks all interaction;readonlykeeps browsing but blocks selection - ↕️ Fixed weeks —
fixedWeeksrenders a consistent 6-row grid;initialFocusmoves focus into the grid on mount - 🖱 Live hover preview —
data-highlighted/data-highlighted-start/data-highlighted-endattributes drive range preview styling while picking the end
Component family
SCalendarRange— the styled wrapper that forwards props to the Aria compact, injectscalendarRangeVariantsclasses (12 slots, shared button icon recipes), and renders the default month/year Select controls in the headingCalendarRangeCompact(Aria) — data-driven composition ofCalendarRangeRoot+ header (prev/heading/next) + one grid per month (grid head/body rows ofCalendarRangeCellTrigger); import from@vean/aria/calendar-rangefor unstyled usageCalendarRangeRoot(Aria) — state owner:useControllableStateforDateRange/placeholder,useCalendarfor grid/month pages,useCalendarRangeStatefor selected/highlighted/invalid state and candidate-range validationCalendarRangeCellTrigger/CalendarRangeCell/CalendarRangeGrid*(Aria) — the editable day button (keyboard handling, focus management, range data attributes) and its semantic grid cell wrappers
Demos
Basic
| M | T | W | T | F | S | S |
|---|---|---|---|---|---|---|
| M | T | W | T | F | S | S |
|---|---|---|---|---|---|---|
Range: Sat Apr 18 2026 00:00:00 GMT+0000 (Coordinated Universal Time) ~ Wed Apr 22 2026 00:00:00 GMT+0000 (Coordinated Universal Time)
Disabled
| M | T | W | T | F | S | S |
|---|---|---|---|---|---|---|
Custom Styling
| M | T | W | T | F | S | S |
|---|---|---|---|---|---|---|
API
Aria Layer
14CalendarRange
Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | ClassValue | — | Additional class names applied to the root element. |
size | ThemeSize | — | Visual size of the component. |
ui | Partial<CalendarRangeUi> | — | Per-slot class overrides for the component. |
headerProps | CalendarRangeHeaderProps | — | Properties forwarded to the header element. |
headingProps | CalendarRangeHeadingProps | — | Properties forwarded to the heading element. |
prevProps | CalendarRangePrevProps | — | Properties forwarded to the prev element. |
nextProps | CalendarRangeNextProps | — | Properties forwarded to the next element. |
gridProps | CalendarRangeGridProps | — | Properties forwarded to the grid element. |
gridHeadProps | CalendarRangeGridHeadProps | — | Properties forwarded to the grid head element. |
gridBodyProps | CalendarRangeGridBodyProps | — | Properties forwarded to the grid body element. |
gridRowProps | CalendarRangeGridRowProps | — | Properties forwarded to the grid row element. |
headCellProps | CalendarRangeHeadCellProps | — | Properties forwarded to the head cell element. |
cellProps | CalendarRangeCellProps | — | Properties forwarded to the cell element. |
cellTriggerProps | CalendarRangeCellTriggerProps | — | Properties forwarded to the cell trigger element. |
defaultValue | DateRange | — | Default value. |
defaultPlaceholder | DateValue | — | Default placeholder. |
placeholder | DateValue | — | Placeholder. |
modelValue | DateRange | — | Current model value. |
allowNonContiguousRanges | boolean | — | Whether to allow non contiguous ranges. |
pagedNavigation | boolean | — | Whether paged navigation. |
preventDeselect | boolean | — | Whether prevent deselect. |
maximumDays | number | — | Maximum days. |
weekStartsOn | WeekStartsOn | — | Week starts on. |
weekdayFormat | WeekDayFormat | — | Weekday format. |
calendarLabel | string | — | Calendar label. |
fixedWeeks | boolean | — | Whether fixed weeks. |
maxValue | DateValue | — | Max value. |
minValue | DateValue | — | Min value. |
locale | string | — | Locale. |
numberOfMonths | number | — | Number of months. |
disabled | boolean | — | Whether the component is disabled. |
readonly | boolean | — | Whether the component is readonly. |
initialFocus | boolean | — | Whether initial focus. |
isDateDisabled | DateMatcher | — | Whether the date is disabled. |
isDateUnavailable | DateMatcher | — | Whether the date is unavailable. |
isDateHighlightable | DateMatcher | — | Whether a date highlightable. |
dir | Direction | — | Reading direction of the component. |
nextPage | ((placeholder: DateValue) => DateValue) | — | Next page. |
prevPage | ((placeholder: DateValue) => DateValue) | — | Prev page. |
disableDaysOutsideCurrentView | boolean | — | Whether to disable days outside current view. |
fixedDate | 'start' | 'end' | — | Fixed date. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | DateRange | Emitted when the model value changes. |
update:placeholder | DateValue | Emitted when the placeholder value changes. |
update:startValue | DateValue | null | Emitted when the start value changes. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
default | { modelValue: DateRange; } | Custom content for the default slot. |
prev | { disabled: boolean; } | Custom content for the prev slot. |
heading | CalendarRangeHeadingSlotProps | Custom content for the heading slot. |
next | { disabled: boolean; } | Custom content for the next slot. |
head-cell | { date: Date; index: number; label: string; } | Custom content for the head cell slot. |
day | CalendarRangeDaySlotProps | Custom content for the day slot. |
CalendarRangeCell
Props
| Prop | Type | Default | Description |
|---|---|---|---|
date | DateValue | — | Date. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
CalendarRangeCellTrigger
Props
| Prop | Type | Default | Description |
|---|---|---|---|
day | Date | — | Day. |
month | Date | — | Month. |
type | ButtonType | 'button' | The type of the button element. Can be one of 'button', 'submit', or 'reset'. |
disabled | boolean | — | Whether the component is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
default | CalendarRangeCellTriggerSlotProps | — |
CalendarRangeCompact
Props
| Prop | Type | Default | Description |
|---|---|---|---|
headerProps | CalendarRangeHeaderProps | — | Properties forwarded to the header element. |
headingProps | CalendarRangeHeadingProps | — | Properties forwarded to the heading element. |
prevProps | CalendarRangePrevProps | — | Properties forwarded to the prev element. |
nextProps | CalendarRangeNextProps | — | Properties forwarded to the next element. |
gridProps | CalendarRangeGridProps | — | Properties forwarded to the grid element. |
gridHeadProps | CalendarRangeGridHeadProps | — | Properties forwarded to the grid head element. |
gridBodyProps | CalendarRangeGridBodyProps | — | Properties forwarded to the grid body element. |
gridRowProps | CalendarRangeGridRowProps | — | Properties forwarded to the grid row element. |
headCellProps | CalendarRangeHeadCellProps | — | Properties forwarded to the head cell element. |
cellProps | CalendarRangeCellProps | — | Properties forwarded to the cell element. |
cellTriggerProps | CalendarRangeCellTriggerProps | — | Properties forwarded to the cell trigger element. |
defaultValue | DateRange | — | Default value. |
defaultPlaceholder | DateValue | — | Default placeholder. |
placeholder | DateValue | — | Placeholder. |
modelValue | DateRange | — | Current model value. |
allowNonContiguousRanges | boolean | — | Whether to allow non contiguous ranges. |
pagedNavigation | boolean | — | Whether paged navigation. |
preventDeselect | boolean | — | Whether prevent deselect. |
maximumDays | number | — | Maximum days. |
weekStartsOn | WeekStartsOn | — | Week starts on. |
weekdayFormat | WeekDayFormat | — | Weekday format. |
calendarLabel | string | — | Calendar label. |
fixedWeeks | boolean | — | Whether fixed weeks. |
maxValue | DateValue | — | Max value. |
minValue | DateValue | — | Min value. |
locale | string | — | Locale. |
numberOfMonths | number | — | Number of months. |
disabled | boolean | — | Whether the component is disabled. |
readonly | boolean | — | Whether the component is readonly. |
initialFocus | boolean | — | Whether initial focus. |
isDateDisabled | DateMatcher | — | Whether the date is disabled. |
isDateUnavailable | DateMatcher | — | Whether the date is unavailable. |
isDateHighlightable | DateMatcher | — | Whether a date highlightable. |
dir | Direction | — | Reading direction of the component. |
nextPage | ((placeholder: DateValue) => DateValue) | — | Next page. |
prevPage | ((placeholder: DateValue) => DateValue) | — | Prev page. |
disableDaysOutsideCurrentView | boolean | — | Whether to disable days outside current view. |
fixedDate | 'start' | 'end' | — | Fixed date. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | DateRange | Emitted when the model value changes. |
update:placeholder | DateValue | Emitted when the placeholder value changes. |
update:startValue | DateValue | null | Emitted when the start value changes. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
default | { modelValue: DateRange; } | Custom content for the default slot. |
prev | { disabled: boolean; } | Custom content for the prev slot. |
heading | CalendarRangeHeadingSlotProps | Custom content for the heading slot. |
next | { disabled: boolean; } | Custom content for the next slot. |
head-cell | { date: Date; index: number; label: string; } | Custom content for the head cell slot. |
day | CalendarRangeDaySlotProps | Custom content for the day slot. |
CalendarRangeGrid
Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
CalendarRangeGridBody
Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
CalendarRangeGridHead
Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
CalendarRangeGridRow
Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
CalendarRangeHeadCell
Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
CalendarRangeHeader
Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
CalendarRangeHeading
Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
CalendarRangeNext
Props
| Prop | Type | Default | Description |
|---|---|---|---|
nextPage | ((placeholder: DateValue) => DateValue) | — | Next page. |
type | ButtonType | 'button' | The type of the button element. Can be one of 'button', 'submit', or 'reset'. |
disabled | boolean | — | Whether the component is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
CalendarRangePrev
Props
| Prop | Type | Default | Description |
|---|---|---|---|
prevPage | ((placeholder: DateValue) => DateValue) | — | Prev page. |
type | ButtonType | 'button' | The type of the button element. Can be one of 'button', 'submit', or 'reset'. |
disabled | boolean | — | Whether the component is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
CalendarRangeRoot
Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | DateRange | — | Default value. |
defaultPlaceholder | DateValue | — | Default placeholder. |
placeholder | DateValue | — | Placeholder. |
modelValue | DateRange | — | Current model value. |
allowNonContiguousRanges | boolean | — | Whether to allow non contiguous ranges. |
pagedNavigation | boolean | — | Whether paged navigation. |
preventDeselect | boolean | — | Whether prevent deselect. |
maximumDays | number | — | Maximum days. |
weekStartsOn | WeekStartsOn | — | Week starts on. |
weekdayFormat | WeekDayFormat | — | Weekday format. |
calendarLabel | string | — | Calendar label. |
fixedWeeks | boolean | — | Whether fixed weeks. |
maxValue | DateValue | — | Max value. |
minValue | DateValue | — | Min value. |
locale | string | — | Locale. |
numberOfMonths | number | — | Number of months. |
disabled | boolean | — | Whether the component is disabled. |
readonly | boolean | — | Whether the component is readonly. |
initialFocus | boolean | — | Whether initial focus. |
isDateDisabled | DateMatcher | — | Whether the date is disabled. |
isDateUnavailable | DateMatcher | — | Whether the date is unavailable. |
isDateHighlightable | DateMatcher | — | Whether a date highlightable. |
dir | Direction | — | Reading direction of the component. |
nextPage | ((placeholder: DateValue) => DateValue) | — | Next page. |
prevPage | ((placeholder: DateValue) => DateValue) | — | Prev page. |
disableDaysOutsideCurrentView | boolean | — | Whether to disable days outside current view. |
fixedDate | 'start' | 'end' | — | Fixed date. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | DateRange | Emitted when the model value changes. |
update:placeholder | DateValue | Emitted when the placeholder value changes. |
update:startValue | DateValue | null | Emitted when the start value changes. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
default | CalendarRangeRootSlotProps | Custom content for the default slot. |
Notes
Architecture and benchmark differences
CalendarRangeRoot owns the value via useControllableState (DateRange = { start, end }), keeps the placeholder for grid paging, and delegates grid creation to useCalendar (shared with calendar). useCalendarRangeState derives selected/highlighted state and exposes isRangeInvalid(start, end) — a candidate-range validator used both by the derived data-invalid state and by onDateChange when committing a new range (so a non-contiguous range is rejected based on the candidate, not the previously committed state). CalendarRangeCellTrigger is the only interactive piece: it reads the shared context, computes data-selection-start/data-selection-end/data-highlighted/data-selected/data-today/data-unavailable/data-outside-view, handles Arrow/Enter/Space keys with dir-aware direction (RTL flips ArrowLeft/ArrowRight), and pages to adjacent months when navigation crosses the grid boundary. CalendarRangeCell carries data-in-range for the days of a committed range, which is what the styled band keys on — a lone start is data-selected without a band, so the band can only appear once both ends are picked. Most benchmark libraries ship a monolithic range panel; the Aria/styled split, per-part *Props passthrough, hover preview, and candidate-range validation are the differentiators.
| Capability | VeanUI | Ant Design | Element Plus | Mantine | Naive UI | shadcn |
|---|---|---|---|---|---|---|
| Aria/styled split | ✅ | — | — | — | — | — |
| Range picking (start + end) | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| Hover range preview | ✅ | — | — | ✅ | — | — |
| Keyboard grid navigation | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| RTL direction reversal | ✅ | — | — | — | — | — |
numberOfMonths grid | ✅ | — | — | ✅ | — | — |
allowNonContiguousRanges | ✅ | — | ✅ | ✅ | — | — |
maximumDays cap | ✅ | — | — | ✅ | — | — |
fixedDate pinned end | ✅ | — | — | — | — | — |
minValue/maxValue bounds | ✅ | ✅ | ✅ | ✅ | ✅ | — |
isDateDisabled / unavailable | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| Month/year Select controls | ✅ | ✅ | ✅ | — | — | — |
| Candidate-range validation | ✅ | — | — | — | — | — |
Cautions
- The value is a
DateRange—{ start, end }ofDateValues (nativeDateat local midnight), never strings. - A range with gaps is rejected by default (the second click restarts the range at the clicked day); pass
allowNonContiguousRangesto allow gaps. defaultValue/defaultPlaceholderare read on mount only — usev-modelfor external control.isDateDisabledandisDateUnavailablediffer: disabled dates are unselectable by policy, unavailable dates are additionally marked visually (line-throughin the default style).fixedDatepins one end: withfixedDate="start"re-picking always updates the end; withfixedDate="end"re-picking always updates the start.- The prev/next
aria-labels default to locale messages; override per button throughprevProps/nextProps. - Each day button’s
aria-labeldefaults to the full localized date; override it per cell throughcellTriggerProps. - The range band is cell-level: days inside a committed range carry
data-in-range(a lone start isdata-selectedonly, so it draws no band). Key custom band styling onui.cell+data-in-range, not ondata-selected.
FAQ
How do I select a range?
Click the start date, then the end date — in either order. While picking the end, the hovered span is previewed with the highlighted state. The range is emitted as { start, end } via v-model.
How do I prevent ranges that span unavailable days?
Keep allowNonContiguousRanges off (default): when the second click would create a range crossing a disabled or unavailable day, the selection is rejected and restarts at the clicked day. Set allowNonContiguousRanges to allow gaps.
How do I cap the range length?
Pass maximumDays (inclusive). Clicking an end that exceeds the cap restarts the selection at the clicked day.
How do I keep one end fixed while re-picking?
Pass fixedDate="start" (or "end"). Clicking any day updates the other end, and clicking before/after the pinned end sorts the result automatically.
How do I build a popup range picker?
Use SDateRangePicker, which composes CalendarRangeCompact inside a popover trigger. Use SCalendarRange standalone when the calendar surface is always visible.