Skip to main contentSkip to docs navigation

Range Calendar

Chassis Range Calendar is a standalone date-range grid for selecting a start and end date.

Overview

RangeCalendar is the range-selection counterpart to Calendar — click a start date, then an end date, to select a contiguous range. Hovering (or dragging) between the two previews the range before it's committed. DateRangePicker renders this same component inside its popover, the same way DatePicker renders Calendar.

July 2026
JSX
import { RangeCalendar } from '@chassis-ui/react'
import { CalendarDate } from '@internationalized/date'

export const Example = () => {
  return (
    <RangeCalendar
      aria-label="Trip dates"
      defaultValue={{ start: new CalendarDate(2026, 7, 10), end: new CalendarDate(2026, 7, 15) }}
    />
  )
}

Min and max dates

Restrict the selectable range with minValue/maxValue, same as Calendar. Dates outside the range are shown but disabled, and can't start or extend a selection.

October 2026
JSX
import { RangeCalendar } from '@chassis-ui/react'
import { today, getLocalTimeZone } from '@internationalized/date'

export const Example = () => {
  const now = today(getLocalTimeZone())

  return (
    <RangeCalendar
      aria-label="Trip dates"
      defaultValue={{ start: now, end: now.add({ days: 3 }) }}
      maxValue={now.add({ days: 30 })}
      minValue={now}
    />
  )
}

Unavailable dates

isDateUnavailable and the JSON-friendly unavailableDates (an array of ISO 8601 YYYY-MM-DD strings) work exactly as they do on Calendar, composing when both are given. An unavailable date can't be picked as either endpoint.

October 2026
JSX
import { RangeCalendar } from '@chassis-ui/react'
import { today, getLocalTimeZone } from '@internationalized/date'

export const Example = () => {
  const now = today(getLocalTimeZone())
  const unavailableDates = [now.add({ days: 2 }), now.add({ days: 5 })].map((date) =>
    date.toString()
  )

  return (
    <RangeCalendar
      aria-label="Trip dates"
      defaultValue={{ start: now.add({ days: 3 }), end: now.add({ days: 4 }) }}
      unavailableDates={unavailableDates}
    />
  )
}

Weekend styling

Weekends are shown muted automatically, same as Calendar — locale-aware, no prop required. See Internationalization for how the active locale is set.

Navigation is always month/year dropdowns — there's no plain arrows-only variant to choose between, since a range picker is usually paired with visibleMonths > 1, where jumping several months or years at once is the common case.

First day of week

Same firstDayOfWeek prop as Calendar — the week starts on Monday by default, regardless of the active locale. When visibleMonths > 1, it applies consistently across every visible month.

July 2026
JSX
import { RangeCalendar } from '@chassis-ui/react'
import { CalendarDate } from '@internationalized/date'

export const Example = () => {
  return (
    <RangeCalendar
      aria-label="Trip dates"
      defaultValue={{ start: new CalendarDate(2026, 7, 10), end: new CalendarDate(2026, 7, 15) }}
      firstDayOfWeek="sun"
    />
  )
}

Multiple months

Pass visibleMonths to show more than one month side by side, sharing a single selection — the standard layout for picking a date range. Each month gets its own header with its own month/year dropdowns, kept in sync — picking a month or year from any one of them moves every visible month together. The prev/next buttons move to a single global overlay instead of repeating in every month's header, paging the whole visible span at once. The months wrap onto a new row if their container is too narrow to fit them side by side (driven by the calendar's own width, not the viewport).

October 2026
JSX
import { RangeCalendar } from '@chassis-ui/react'
import { today, getLocalTimeZone } from '@internationalized/date'

export const Example = () => {
  const now = today(getLocalTimeZone())

  return (
    <RangeCalendar
      aria-label="Trip dates"
      defaultValue={{ start: now, end: now.add({ days: 10 }) }}
      visibleMonths={2}
    />
  )
}

Presets

Pass presets — an array of { label, range } entries (see DateRangePreset below) — to show a quick-select list next to the calendar. Selecting a preset commits its range immediately, the same as picking a start and end date from the grid. A preset the calendar couldn't select is disabled: one with no day inside minValue/maxValue, and one with an unavailable day in its range, which a range can't span. The preset matching the current selection (if any) is shown as the active one.

October 2026
JSX
import { RangeCalendar } from '@chassis-ui/react'
import { getLocalTimeZone, startOfMonth, startOfWeek, today } from '@internationalized/date'

export const Example = () => {
  const now = today(getLocalTimeZone())

  const thisWeekStart = startOfWeek(now, 'en-US')
  const lastWeekEnd = thisWeekStart.subtract({ days: 1 })
  const lastWeekStart = startOfWeek(lastWeekEnd, 'en-US')

  const thisMonthStart = startOfMonth(now)
  const lastMonthEnd = thisMonthStart.subtract({ days: 1 })
  const lastMonthStart = startOfMonth(lastMonthEnd)

  const presets = [
    { label: 'Today', range: { start: now, end: now } },
    { label: 'Last 7 Days', range: { start: now.subtract({ days: 6 }), end: now } },
    { label: 'Last 30 Days', range: { start: now.subtract({ days: 29 }), end: now } },
    { label: 'Last 90 Days', range: { start: now.subtract({ days: 89 }), end: now } },
    { label: 'Last Week', range: { start: lastWeekStart, end: lastWeekEnd } },
    { label: 'Last Month', range: { start: lastMonthStart, end: lastMonthEnd } }
  ]

  return (
    <RangeCalendar
      aria-label="Trip dates"
      defaultValue={presets[1].range}
      presets={presets}
      visibleMonths={2}
    />
  )
}

Accessible name

RangeCalendar has no visible label of its own — pass aria-label or aria-labelledby any time it's used standalone. When composed inside DateRangePicker's popover, the accessible name instead comes from the picker's own label.

Styling

Like Calendar, RangeCalendar isn't styled by @chassis-ui/css. It reuses Calendar.scss's header/nav/grid/cell classes directly (so the two always look identical outside of an active selection), adding only the sm amount of CSS needed for the connected "pill" band across a selected or previewed range, and for the multi-month layout (.datepicker-grid/.datepicker-column/.datepicker-controls, the same classes @chassis-ui/css's own datepicker component uses for side-by-side month grids with a shared prev/next pair) — in @chassis-ui/react/style.css, which an app imports once (see Theming).

Scope

  • Contiguous ranges only — allowsNonContiguousRanges (a react-aria option for ranges that may skip over unavailable dates) isn't exposed.

API

RangeCalendar

PropTypeDefault
aria-label?string—
An accessible label for the calendar, used when there's no visible label. Required for standalone use — a calendar grid has no other accessible name of its own.
aria-labelledby?string—
Identifies a visible label element for the calendar.
autoFocus?boolean—
Whether to automatically focus the calendar when it mounts.
className?string—
A string of all className you want applied to the base component.
defaultFocusedValue?DateValue | null—
The date the calendar first shows and focuses, in place of the selected date or today. DatePicker and DateRangePicker pass their placeholderValue here while empty.
defaultValue?RangeValue<DateValue> | null—
The initial selected date range (uncontrolled).
disabled?boolean—
Prevents the calendar from being focused or interacted with.
firstDayOfWeek?"sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat"mon
The day that starts the week, overriding the default set by the active locale.
isDateUnavailable?((date: DateValue) => boolean)—
Callback that is called for each date in the calendar. If it returns true, that date is shown but cannot be selected.
labels?Partial<CalendarLabels>—
Overrides for the handful of strings this component renders itself rather than getting from the active locale (the year view's paging arrows, its live-region announcements, and the month/year header buttons). Everything else — month and weekday names, the calendar system — follows I18nProvider's locale via react-aria and needs no override. Merged over the English defaults, so passing one key leaves the rest alone.
maxValue?DateValue | null—
The maximum allowed date that a user may select.
minValue?DateValue | null—
The minimum allowed date that a user may select.
onChange?((value: RangeValue<DateValue>) => void)—
Callback fired when a complete range is selected (both a start and end date).
presets?DateRangePreset[]—
A list of quick-select range presets shown beside the calendar. Selecting a preset commits its range immediately, the same as picking a start and end date from the grid: in order, kept inside minValue/maxValue, and in the calendar of the current value with its times. A preset the grid couldn't select is disabled: one with no day inside those limits, and one with an unavailable day in its range. The preset matching the current selection (if any) is marked selected. Omit to not show a preset list.
unavailableDates?string[]—
ISO 8601 dates (YYYY-MM-DD) to mark unselectable, as a convenience alternative to isDateUnavailable for data-driven cases (e.g. booked dates fetched from an API). Composed with isDateUnavailable when both are given — a date unavailable by either is unavailable.
value?RangeValue<DateValue> | null—
The selected date range (controlled).
visibleMonths?number1
Number of months to display side by side, sharing one selection. Wraps to multiple rows in a narrow container (e.g. a popover on a sm screen) rather than overflowing — the wrap is driven by the calendar's own width, not the viewport, so it adapts correctly regardless of where the calendar is embedded.

DateRangePreset

Shape of each entry in the presets array.

PropTypeDefault
labelstring—
rangeRangeValue<DateValue>—