Open UI

DateRangePicker

A date range picker combines two DateFields and a RangeCalendar popover to allow users to enter or select a date and time range.

import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  return <DateRangePicker label="Event date" />}

Usage

Use DateRangePicker for a date-range input with a calendar popover. Use DatePicker for a single date. Use RangeCalendar for inline range selection.

import { DateRangePicker } from "@opengovsg/oui"
<DateRangePicker label="Booking period" />

Alternatively, install the component as local source via the shadcn CLI:

npx shadcn@latest add https://oui.open.gov.sg/r/date-range-picker.json
pnpm dlx shadcn@latest add https://oui.open.gov.sg/r/date-range-picker.json
npx shadcn@latest add https://oui.open.gov.sg/r/date-range-picker.json
bunx --bun shadcn@latest add https://oui.open.gov.sg/r/date-range-picker.json

DateRangePicker combines two DateField inputs (start and end) with a RangeCalendar in a popover, built on React Aria's DateRangePicker. Date values use objects from the @internationalized/date package.

If no visible label is provided, an aria-label or aria-labelledby prop must be passed to identify the field to assistive technology.

Examples

Disabled

import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  return <DateRangePicker label="Event date" isDisabled />}

Read Only

import { CalendarDate } from "@internationalized/date"import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  return (    <DateRangePicker      label="Event date"      value={{        start: new CalendarDate(2024, 7, 1),        end: new CalendarDate(2024, 7, 8),      }}      isReadOnly    />  )}

Required

import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  return <DateRangePicker label="Event date" isRequired />}

With Description

You can add a description to the DateRangePicker by passing the description property.

import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  return (    <DateRangePicker      label="Event date"      description="Select the start and end dates for your event."    />  )}

With Error Message

You can combine the isInvalid and errorMessage properties to show an invalid input.

import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  return (    <DateRangePicker      label="Event date"      isInvalid      errorMessage="Please enter a valid date range."    />  )}

With Time Fields

If the value (or defaultValue) given to DateRangePicker includes time information, time fields will be shown in the DateFields.

import { parseZonedDateTime } from "@internationalized/date"import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  return (    <DateRangePicker      label="Meeting time"      defaultValue={{        start: parseZonedDateTime("2022-11-07T10:45[America/Los_Angeles]"),        end: parseZonedDateTime("2022-11-08T11:15[America/Los_Angeles]"),      }}    />  )}

Controlled

You can use the value and onChange properties to control the input value.

import { useState } from "react"import { CalendarDate } from "@internationalized/date"import { useDateFormatter } from "@react-aria/i18n"import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  const [value, setValue] = useState<{    start: CalendarDate    end: CalendarDate  } | null>({    start: new CalendarDate(2024, 7, 1),    end: new CalendarDate(2024, 7, 8),  })  const formatter = useDateFormatter({ dateStyle: "long" })  return (    <div className="flex flex-col gap-4">      <DateRangePicker label="Event date" value={value} onChange={setValue} />      <p className="text-muted-foreground text-sm">        Selected date range:{" "}        {value          ? `${formatter.format(value.start.toDate("UTC"))} to ${formatter.format(value.end.toDate("UTC"))}`          : "--"}      </p>    </div>  )}

Time Zones

DateRangePicker is timezone aware when a ZonedDateTime object is provided as the value. In this case, the time zone abbreviation is displayed, and time zone concerns such as daylight saving time are taken into account when the value is manipulated.

@internationalized/date includes functions for parsing strings in multiple formats into ZonedDateTime objects.

npm install @internationalized/date
import { parseZonedDateTime } from "@internationalized/date"
import { parseZonedDateTime } from "@internationalized/date"import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  return (    <DateRangePicker      label="Meeting time"      defaultValue={{        start: parseZonedDateTime("2022-11-07T00:45[America/Los_Angeles]"),        end: parseZonedDateTime("2022-11-08T11:15[America/Los_Angeles]"),      }}    />  )}

Granularity

The granularity prop allows you to control the smallest unit that is displayed by DateRangePicker. By default, the value is displayed with "day" granularity (year, month, and day), and CalendarDateTime and ZonedDateTime values are displayed with "minute" granularity.

@internationalized/date includes functions for parsing strings in multiple formats into ZonedDateTime objects.

npm install @internationalized/date
import { DateValue, parseAbsoluteToLocal } from "@internationalized/date"
import { useDateFormatter } from "@react-aria/i18n"
import type { ZonedDateTime } from "@internationalized/date"import { useState } from "react"import { parseAbsoluteToLocal } from "@internationalized/date"import { useDateFormatter } from "@react-aria/i18n"import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  const [value, setValue] = useState<{    start: ZonedDateTime    end: ZonedDateTime  } | null>({    start: parseAbsoluteToLocal("2021-04-07T18:45:22Z"),    end: parseAbsoluteToLocal("2021-04-08T20:00:00Z"),  })  const formatter = useDateFormatter({ dateStyle: "short", timeStyle: "long" })  return (    <div className="flex flex-col gap-4">      <DateRangePicker        label="Date and time range"        granularity="second"        value={value}        onChange={setValue}      />      <p className="text-muted-foreground text-sm">        Selected date range:{" "}        {value && value.start && value.end          ? `${formatter.format(value.start.toDate())} to ${formatter.format(value.end.toDate())}`          : "--"}      </p>    </div>  )}

Min Date And Max Date

The minValue and maxValue props can also be used to ensure the value is within a specific range.

You may also want to use RangeCalendar's isDateUnavailable (via calendarProps) prop to visually indicate which dates are unavailable for selection in the calendar.

@internationalized/date includes functions for parsing strings in multiple formats into ZonedDateTime objects.

npm install @internationalized/date
import { getLocalTimeZone, parseDate, today } from "@internationalized/date"
import { getLocalTimeZone, parseDate, today } from "@internationalized/date"import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  return (    <DateRangePicker      label="Trip dates"      minValue={today(getLocalTimeZone())}      maxValue={parseDate("2025-12-31")}      defaultValue={{        start: parseDate("2025-07-03"),        end: parseDate("2025-07-10"),      }}      calendarProps={{        isDateUnavailable: (date) =>          date.compare(today(getLocalTimeZone())) < 0 ||          date.compare(parseDate("2025-12-31")) > 0,      }}    />  )}

International Calendar

DateRangePicker supports selecting dates in many calendar systems used around the world, including Gregorian, Hebrew, Indian, Islamic, Buddhist, and more. Dates are automatically displayed in the appropriate calendar system for the user's locale. The calendar system can be overridden using the https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar#adding_a_calendar_in_the_locale_string, passed to the I18nProvider component.

@internationalized/date includes functions for parsing strings in multiple formats into ZonedDateTime objects.

npm install @internationalized/date
import { parseAbsoluteToLocal } from "@internationalized/date"
import { I18nProvider } from "@react-aria/i18n"
import { parseAbsoluteToLocal } from "@internationalized/date"import { I18nProvider } from "@react-aria/i18n"import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  return (    <I18nProvider locale="hi-IN-u-ca-indian">      <DateRangePicker        label="Date range"        defaultValue={{          start: parseAbsoluteToLocal("2021-04-01T18:45:22Z"),          end: parseAbsoluteToLocal("2021-04-08T18:45:22Z"),        }}      />    </I18nProvider>  )}

Unavailable Dates

DateRangePicker supports marking certain dates as unavailable. These dates cannot be selected by the user and are displayed with a crossed out appearance in the calendar. In the date field, an invalid state is displayed if a user enters an unavailable date.

@internationalized/date includes functions for parsing strings in multiple formats into ZonedDateTime objects.

npm install @internationalized/date
import { getLocalTimeZone, isWeekend, today } from "@internationalized/date"
import { useLocale } from "@react-aria/i18n"
import type { DateValue } from "@internationalized/date"import { getLocalTimeZone, isWeekend, today } from "@internationalized/date"import { useLocale } from "@react-aria/i18n"import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  const { locale } = useLocale()  const now = today(getLocalTimeZone())  const disabledRanges = [    [now, now.add({ days: 5 })],    [now.add({ days: 14 }), now.add({ days: 16 })],    [now.add({ days: 23 }), now.add({ days: 24 })],  ]  const isDateUnavailable = (date: DateValue) =>    isWeekend(date, locale) ||    disabledRanges.some(      (interval) =>        date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0,    )  return (    <DateRangePicker      label="Trip dates"      minValue={today(getLocalTimeZone())}      calendarProps={{        isDateUnavailable,      }}    />  )}

Visible Months

By default, the calendar popover displays two months. You can use the calendarProps#visibleDuration to pass a custom duration to display more months at once.

import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  return (    <DateRangePicker      label="Event date"      calendarProps={{ visibleDuration: { months: 3 } }}    />  )}

Custom first day of the week

By default, the first day of the week is determined by the user's locale. You can override this using the calendarProps#firstDayOfWeek prop. The available values are 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', and 'sat'.

import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  return (    <DateRangePicker      label="Event date"      calendarProps={{ firstDayOfWeek: "mon" }}    />  )}

Page Behavior

By default, when pressing the next or previous buttons, pagination will advance by a single month. This behavior can be changed to page by the visible months instead, by setting calendarProps#pageBehavior to "visible".

import { DateRangePicker } from "@opengovsg/oui"export const Example = () => {  return (    <DateRangePicker      label="Event date"      calendarProps={{        visibleDuration: { months: 2 },        pageBehavior: "visible",      }}    />  )}

Validation

See the Forms guide for end-to-end patterns including React Hook Form + Zod.

  • isRequired — Marks the field as required; validation fires on form submit (native) or immediately (aria).
  • validate — A custom function (value: RangeValue<DateValue> | null) => ValidationError | true | null | undefined. Return a string to show as the error.
  • isInvalid + errorMessage — Use together for realtime validation feedback. errorMessage accepts a string or a function (validation: ValidationResult) => string.
  • minValue / maxValue — Constrain the accepted date range.
  • validationBehavior"native" (default) blocks form submission when invalid; "aria" marks the field via ARIA attributes only (needed for React Hook Form).

Events

  • onChange — Called when the date range value changes, receiving a RangeValue<DateValue> (or null).
  • onOpenChange — Called when the calendar popover opens or closes, receiving a boolean.
  • onFocus / onBlur — Called when the field receives or loses focus.
  • onFocusChange — Called when the focus state changes, receiving a boolean.

Accessibility

  • Each DateField segment uses role="spinbutton" with appropriate ARIA labels; assistive technologies announce the segment name and current value.
  • The calendar button opens the calendar popover; the popover is labelled automatically.
  • Keyboard interaction: Tab to move between segments and the calendar button; type digits to update segments; Arrow Up/Down to increment/decrement; Enter on the calendar button opens the popover.
  • The label, description, and error message are linked via aria-labelledby and aria-describedby.
  • If no visible label is provided, pass aria-label or aria-labelledby.

Slots

Slots are named regions of the component you can target with custom Tailwind classes via the classNames prop. Each slot below corresponds to a key on the classNames object.

  • base: Input wrapper, it handles alignment, placement, and general appearance.
  • group: Wrapper for the input fields and calendar button.
  • dateWrapper: Wrapper for the two date input fields.
  • startInput: The start date input component element.
  • connector: The dash connector between the two date inputs.
  • endInput: The end date input component element.
  • calendarButton: The calendar button element.
  • dialog: The calendar dialog container inside the popover element.

You can also override styles of the various components used within DateRangePicker:

RangeCalendar slots

Slots can be overridden via the classNames.calendar prop or calendarProps#classNames prop.

Popover slots

Slots can be overridden via the classNames.popover prop or popoverProps#classNames prop.

FieldError slots

Slots can be overridden via the classNames.error prop.

Props

DateRangePicker

PropTypeDefaultDescription
labelstring-The visible label for the field
descriptionstring-Helper text shown below the field
errorMessagestring | ((validation: ValidationResult) => string)-Error message displayed when the field is invalid
valueRangeValue<DateValue> | null-The current date range value (controlled)
defaultValueRangeValue<DateValue> | null-The default date range value (uncontrolled)
onChange(value: RangeValue<DateValue> | null) => void-Called when the date range changes
isDisabledbooleanfalseWhether the field is disabled
isReadOnlybooleanfalseWhether the field is read-only
isRequiredbooleanfalseWhether the field is required
isInvalidbooleanfalseWhether the field should display as invalid
validate(value: RangeValue<DateValue> | null) => ValidationError | true | null | undefined-Custom validation function
validationBehavior"native" | "aria""native"Whether to use native HTML form validation or ARIA-only
calendarPropsRangeCalendarProps<T>-Props forwarded to the internal RangeCalendar component
calendarButtonPropsButtonProps-Additional props spread to the calendar trigger button (escape hatch)
popoverPropsPopoverProps-Props forwarded to the internal Popover component
classNamesSlotsToClasses<DateRangePickerSlots> & { calendar?: SlotsToClasses<CalendarSlots> }-Custom Tailwind classes for each slot, including nested RangeCalendar slots

Inherits all remaining props from React Aria's DateRangePicker. Notable ones: autoFocus, granularity, hideTimeZone, hourCycle, pageBehavior, allowsNonContiguousRanges.

On this page