Date Range Picker
Two segmented text fields with a range-calendar popover for entering or picking a date range.
Overview
DateRangePicker combines two individually-editable segmented date fields (start and end, each day/month/year) with a range-calendar popover — type both dates directly, or pick a start and end from the grid. All three stay in sync with a single selected range. The popover renders RangeCalendar, which is also available on its own wherever an inline, non-popover range calendar is needed.
import { DateRangePicker } from '@chassis-ui/react'
export const Example = () => {
return <DateRangePicker aria-label="Trip dates" />
}As a form field
Pass label/help to wrap the picker in a .form-field with an associated FormLabel and FormHelp. Pass name to have DateRangePicker create and manage a pair of hidden inputs for native form submission — ${name}Start and ${name}End, each an ISO 8601 date string (2026-07-24), with a time 2026-07-24T09:30:00 (see Time).
With required, a picker with its start or its end missing holds its form back from submitting: the empty field takes focus and the picker shows as invalid, with its invalidFeedback, until it has both dates or the form is reset. invalid, once set, and valid decide how it looks instead.
import { DateRangePicker } from '@chassis-ui/react'
export const Example = () => {
return (
<DateRangePicker
label="Trip dates"
help="We’ll send a reminder the day before you leave."
name="tripDates"
/>
)
}Min & max dates
Restrict the selectable range with minValue/maxValue. Dates outside the range are shown but disabled in the calendar grid. A date typed outside the range, or an end before the start, marks the field invalid, as invalid does, and shows invalidFeedback if there is one.
import { DateRangePicker } from '@chassis-ui/react'
import { today, getLocalTimeZone } from '@internationalized/date'
export const Example = () => {
const now = today(getLocalTimeZone())
return <DateRangePicker aria-label="Trip dates" maxValue={now.add({ days: 60 })} minValue={now} />
}Time
Pass granularity as 'hour', 'minute' or 'second' to add a time to both fields, as on DatePicker: the range's dates become CalendarDateTimes, or ZonedDateTimes from a zoned value, defaultValue or placeholderValue. The calendar picks the days and the fields' segments edit the times. Picking a range keeps both times already set; in an empty field both take the time and zone of placeholderValue, else midnight with no zone. A zoned range therefore needs a zoned placeholderValue to stay zoned after it is cleared. Here the placeholder starts both at 3 PM.
import { DateRangePicker } from '@chassis-ui/react'
import { getLocalTimeZone, toCalendarDateTime, today } from '@internationalized/date'
export const Example = () => {
// Today at 3 PM: the time both dates start from, and the month the calendar opens on.
const checkIn = toCalendarDateTime(today(getLocalTimeZone())).set({ hour: 15 })
return (
<DateRangePicker
granularity="minute"
help="Check-in from 3 PM, check-out by 11 AM."
label="Stay"
name="stay"
placeholderValue={checkIn}
/>
)
}Unavailable dates
Pass isDateUnavailable, or the JSON-friendly unavailableDates (an array of ISO 8601 YYYY-MM-DD strings), to mark specific dates as unselectable without excluding them from the grid. Unavailable dates remain focusable but can't be selected — this applies to both the calendar popover and typing a date directly into either field.
import { DateRangePicker } 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 <DateRangePicker aria-label="Trip dates" unavailableDates={unavailableDates} />
}First day of week
The calendar popover's week starts on Monday by default, regardless of the active locale. Pass firstDayOfWeek to override it — passed straight through to the underlying RangeCalendar. See Internationalization for how the active locale is set.
Multiple months
Pass visibleMonths to show more than one month side by side in the popover, sharing a single selection — passed straight through to the underlying RangeCalendar.
import { DateRangePicker } from '@chassis-ui/react'
export const Example = () => {
return <DateRangePicker aria-label="Trip dates" visibleMonths={2} />
}Presets
Pass presets — an array of { label, range } entries (see DateRangePreset) — to show a quick-select list next to the calendar in the popover. Selecting a preset commits its range immediately, the same as picking a start and end date from the calendar. 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.
import { DateRangePicker } 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 (
<DateRangePicker aria-label="Trip dates" defaultValue={presets[1].range} presets={presets} />
)
}Controlling the popover
The popover manages its own open state by default. Pass visible (with onVisibleChange) to drive it from outside instead — e.g. to open the calendar from a button elsewhere on the page. defaultVisible sets the initial open state for the uncontrolled case, without taking over control. The earlier names isOpen, onOpenChange and defaultOpen still work and are deprecated.
import { useState } from 'react'
import { Button, DateRangePicker } from '@chassis-ui/react'
export const Example = () => {
const [visible, setVisible] = useState(false)
return (
<div className="vstack gap-sm">
<div className="d-flex flex-wrap align-items-center gap-sm">
<DateRangePicker aria-label="Trip dates" visible={visible} onVisibleChange={setVisible} />
<Button color="secondary" onClick={() => setVisible(true)} type="button">
Open calendar
</Button>
</div>
<div className="form-help">Popover is {visible ? 'open' : 'closed'}.</div>
</div>
)
}Form submission
Unlike DatePicker (one hidden input), DateRangePicker creates two hidden inputs when name is set: ${name}Start and ${name}End. This was chosen over a single combined value so each endpoint posts as an ordinary named field — no custom parsing needed on the receiving end.
Styling
Like DatePicker, DateRangePicker isn't styled by @chassis-ui/css. It reuses DatePicker's segmented field and popover shell, adding only a small separator between the two fields. The range-calendar grid itself is styled by RangeCalendar's own scoped CSS. All of it is in @chassis-ui/react/style.css, which an app imports once (see Theming).
Scope
A few things are deliberately out of scope for this pass:
- A time picker in the popover — the times are edited in the fields' segments, and the popover picks the days only.
- Non-contiguous ranges — react-aria's
allowsNonContiguousRangesoption isn't exposed.
API
DateRangePicker
| Prop | Type | Default |
|---|---|---|
aria-label? | string | — |
An accessible label for the date range picker, used when there's no visible <label>. | ||
aria-labelledby? | string | — |
Identifies a visible <label> element for the date range picker. | ||
className? | string | — |
| A string of all className you want applied to the base component. | ||
defaultOpen? | boolean | — |
Whether the calendar popover is open by default (uncontrolled).
@deprecated Use defaultVisible. | ||
defaultValue? | RangeValue<DateValue> | null | — |
| The initial selected date range (uncontrolled). | ||
defaultVisible? | boolean | false |
Whether the calendar popover is open when the date range picker first renders. Use it
instead of visible when nothing outside needs to control the popover. | ||
disabled? | boolean | — |
| Prevents the date range picker from being focused or interacted with. | ||
firstDayOfWeek? | "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | 'mon' |
| The day that starts the week in the calendar overlay, overriding the default set by the active locale. | ||
granularity? | "day" | "hour" | "minute" | "second" | 'day', or 'minute' for a range with times |
The smallest unit the fields show and edit. 'hour', 'minute' and 'second' add a time
to both dates: the range's dates are then CalendarDateTimes, or ZonedDateTimes when
value, defaultValue or placeholderValue holds them, and a range or placeholder given
must carry times. Picking a range in the calendar keeps the times. A range picked in an
empty field takes the time, and zone, of placeholderValue, else midnight with no zone: a
zoned range, once cleared, needs a zoned placeholderValue to stay zoned. | ||
help? | ReactNode | — |
| A description for the field, rendered below the date range picker. | ||
hideTimeZone? | boolean | — |
Hide the time zone of ZonedDateTime dates. | ||
hourCycle? | 12 | 24 | — |
| Show 12 or 24 hours. Defaults to the locale's. | ||
id? | string | — |
id forwarded to the field's grouping element — useful for pairing with a <label for>. | ||
invalid? | boolean | — |
| Set component validation state to invalid. | ||
invalidFeedback? | ReactNode | — |
An error message for the field, rendered below the date range picker when invalid is set,
and after a submit that a required date range picker with a date missing held back. | ||
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. | ||
isOpen? | boolean | — |
Whether the calendar popover is open (controlled).
@deprecated Use visible. | ||
label? | ReactNode | — |
The field's caption, rendered as a FormLabel associated with the field group. | ||
labels? | Partial<CalendarLabels> | — |
Overrides for the strings this component and its calendar render themselves rather than
getting from the active locale — the calendar trigger, the clear adornment and the calendar's
own year-view arrows/announcements. Date segment order, month and weekday names all follow
I18nProvider's locale via react-aria and need 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. | ||
name? | string | — |
Base name for a pair of auto-created hidden inputs, kept in sync with the selection, for
native form submission — rendered as ${name}Start and ${name}End. Omit to skip creating
them. Each value is the date's toString(): 2026-03-15, with a time 2026-03-15T09:30:00,
and with a time zone 2026-03-15T09:30:00+09:00[Asia/Tokyo], which parseZonedDateTime
reads back. | ||
onChange? | ((value: RangeValue<DateValue> | null) => void) | — |
Callback fired when the selected date range changes. With a time (granularity), its dates
are CalendarDateTimes or ZonedDateTimes. | ||
onOpenChange? | ((isOpen: boolean) => void) | — |
Callback fired when the calendar popover's open state changes.
@deprecated Use onVisibleChange. | ||
onVisibleChange? | ((visible: boolean) => void) | — |
Callback fired when the calendar popover asks to open or close: the calendar button, a
completed selection, the Escape key or a click outside. Receives the state it asks for.
With visible set, the popover changes only when visible does. | ||
placeholderValue? | DateValue | — |
The date both fields start from while empty, such as new CalendarDateTime(2026, 1, 1, 9),
and the month the calendar opens on. Its time is the one a range picked in the calendar gets,
and its type the type of the dates onChange receives: a ZonedDateTime gives zoned dates.
With a time granularity it must carry a time. | ||
presets? | DateRangePreset[] | — |
A list of quick-select range presets shown in the overlay next to the calendar. Selecting a
preset commits its range immediately, the same as picking a start and end date from the
calendar: in order, kept inside minValue/maxValue, and with the times of the current
range. A preset the calendar 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. | ||
required? | boolean | — |
Toggle the required state for the component. A required date range picker with its start or
its end missing holds its form back from submitting, takes focus in the field that is empty
and shows as invalid, with invalidFeedback, until it has both dates or the form is reset.
invalid, once set, and valid decide how it looks instead. | ||
shouldForceLeadingZeros? | boolean | — |
Show a leading zero on the day, month and hour, as 03/05 rather than 3/5, whatever the
locale does. | ||
size? | "sm" | "lg" | — |
| Size the component sm or lg. | ||
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.
Applies to both the calendar overlay and typing a date directly into either field. | ||
valid? | boolean | — |
| Set component validation state to valid. | ||
validFeedback? | ReactNode | — |
A success message for the field, rendered below the date range picker when valid is set. | ||
value? | RangeValue<DateValue> | null | — |
| The selected date range (controlled). | ||
visible? | boolean | — |
Whether the calendar popover is open. Setting it makes the popover controlled: it opens and
closes only when this changes, so pair it with onVisibleChange. | ||
visibleMonths? | number | 1 |
| Number of months to display side by side in the calendar overlay. | ||