Skip to main contentSkip to docs navigation

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.

JSX
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.

We’ll send a reminder the day before you leave.
JSX
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.

JSX
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.

Check-in from 3 PM, check-out by 11 AM.
JSX
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.

JSX
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.

JSX
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.

JSX
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.

Popover is closed.
JSX
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 allowsNonContiguousRanges option isn't exposed.

API

DateRangePicker

PropTypeDefault
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?booleanfalse
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?number1
Number of months to display side by side in the calendar overlay.