RangeCalendar
A range calendar displays one or more date grids and allows users to select a contiguous range of dates.
Usage
Use RangeCalendar for inline date-range selection. Use DateRangePicker when the range UI should appear in a popover. Use Calendar for a single date.
Alternatively, install the component as local source via the shadcn CLI:
npx shadcn@latest add https://oui.open.gov.sg/r/range-calendar.jsonpnpm dlx shadcn@latest add https://oui.open.gov.sg/r/range-calendar.jsonnpx shadcn@latest add https://oui.open.gov.sg/r/range-calendar.jsonbunx --bun shadcn@latest add https://oui.open.gov.sg/r/range-calendar.jsonA RangeCalendar has no selection by default. An initial, uncontrolled value can be provided to the RangeCalendar using the defaultValue prop. Alternatively, a controlled value can be provided using the value prop.
Date values are provided using objects in the @internationalized/date package. This library handles correct international date manipulation across calendars, time zones, and other localization concerns.
import { getLocalTimeZone, today } from "@internationalized/date"import { RangeCalendar } from "@opengovsg/oui"export const Example = () => { return ( <div className="flex gap-x-4"> <RangeCalendar aria-label="Date (No Selection)" /> <RangeCalendar aria-label="Date (Uncontrolled)" defaultValue={{ start: today(getLocalTimeZone()), end: today(getLocalTimeZone()).add({ weeks: 1 }), }} /> </div> )}Disabled
The isDisabled boolean prop makes the RangeCalendar disabled. Cells cannot be focused or selected.
import { RangeCalendar } from "@opengovsg/oui"export const Example = () => { return <RangeCalendar isDisabled aria-label="Disabled state" />}Controlled
A RangeCalendar has no selection by default. An initial, uncontrolled value can be provided to the RangeCalendar using the defaultValue prop. Alternatively, a controlled value can be provided using the value prop.
import React from "react"import { getLocalTimeZone, today } from "@internationalized/date"import { RangeCalendar } from "@opengovsg/oui"export const Example = () => { const [value, setValue] = React.useState({ start: today(getLocalTimeZone()), end: today(getLocalTimeZone()).add({ weeks: 1 }), }) return ( <div className="flex flex-col gap-4"> <RangeCalendar aria-label="Date (Controlled)" value={value} onChange={setValue} /> <div> Selected dates: {value.start.toString()} - {value.end.toString()} </div> </div> )}Min Date Value
The minValue can be used to prevent the user from selecting dates outside a certain range.
By default, RangeCalendar allows selecting any date between 1 Jan 1900 and 31 December 2100. Override this with the minValue prop.
This example only accepts dates after today.
import { getLocalTimeZone, today } from "@internationalized/date"import { RangeCalendar } from "@opengovsg/oui"export const Example = () => { return ( <RangeCalendar aria-label="Date (Min Date Value)" minValue={today(getLocalTimeZone())} /> )}Max Date Value
The maxValue can also be used to prevent the user from selecting dates outside a certain range.
By default, RangeCalendar allows selecting any date between 1 Jan 1900 and 31 December 2100. Override this with the maxValue prop.
This example only accepts dates before today.
import { getLocalTimeZone, today } from "@internationalized/date"import { RangeCalendar } from "@opengovsg/oui"export const Example = () => { return ( <RangeCalendar aria-label="Date (Max Date Value)" maxValue={today(getLocalTimeZone())} /> )}Unavailable Dates
RangeCalendar supports marking certain dates as unavailable. These dates remain focusable with the keyboard so that navigation is consistent, but cannot be selected by the user. The isDateUnavailable prop accepts a callback that is called to evaluate whether each visible date is unavailable.
By default, unavailable dates are displayed with a strike-through style. This can be overridden by the classNames#cell prop:
<RangeCalendar
classNames={{
cell: "unavailable:no-underline unavailable:text-red-300",
}}
/>import type { DateValue } from "@internationalized/date"import { getLocalTimeZone, isWeekend, today } from "@internationalized/date"import { useLocale } from "@react-aria/i18n"import { RangeCalendar } from "@opengovsg/oui"export const Example = () => { 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 { locale } = useLocale() const isDateUnavailable = (date: DateValue) => isWeekend(date, locale) || disabledRanges.some( (interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0, ) return ( <RangeCalendar aria-label="Date (Unavailable)" isDateUnavailable={isDateUnavailable} /> )}Non-Contiguous Ranges
The allowsNonContiguousRanges prop enables a range to be selected even if there are unavailable dates in the middle. The value emitted in the onChange event will still be a single range with a start and end property, but unavailable dates will not be displayed as selected. It is up to applications to split the full selected range into multiple as needed for business logic.
This example prevents selecting weekends, but allows selecting ranges that span multiple weeks.
import { isWeekend } from "@internationalized/date"import { useLocale } from "@react-aria/i18n"import { RangeCalendar } from "@opengovsg/oui"export const Example = () => { const { locale } = useLocale() return ( <RangeCalendar allowsNonContiguousRanges aria-label="Time off request" isDateUnavailable={(date) => isWeekend(date, locale)} /> )}Invalid Date
This example validates that the selected date is a weekday and not a weekend according to the current locale.
import { useState } from "react"import { getLocalTimeZone, isWeekend, today } from "@internationalized/date"import { useLocale } from "@react-aria/i18n"import { RangeCalendar } from "@opengovsg/oui"export const Example = () => { const [date, setDate] = useState({ start: today(getLocalTimeZone()), end: today(getLocalTimeZone()).add({ weeks: 1 }), }) const { locale } = useLocale() const isInvalid = isWeekend(date.start, locale) || isWeekend(date.end, locale) return ( <RangeCalendar aria-label="Date (Invalid on weekends)" errorMessage={isInvalid ? "We are closed on weekends" : undefined} isInvalid={isInvalid} value={date} onChange={setDate} /> )}International Calendars
RangeCalendar 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 Unicode calendar locale extension, passed to the Provider component.
import { I18nProvider } from "@react-aria/i18n"import { RangeCalendar } from "@opengovsg/oui"export const Example = () => { return ( <I18nProvider locale="ta-SG"> <RangeCalendar aria-label="Date (International RangeCalendar)" /> </I18nProvider> )}Visible Duration
By default, the RangeCalendar displays a single month. The visibleDuration prop can be used to display multiple months (or specific number of days and weeks) at once.
Going above 3 months is not recommended as UX may be affected due to clutter.
import { RangeCalendar } from "@opengovsg/oui"export const Example = () => { return ( <RangeCalendar aria-label="Date (Visible Duration)" visibleDuration={{ months: 3 }} /> )}Custom first day of week
By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the firstDayOfWeek prop to 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', or 'sat'.
import { RangeCalendar } from "@opengovsg/oui"export const Example = () => { return ( <RangeCalendar aria-label="Date (firstDayOfWeek)" firstDayOfWeek="mon" /> )}Page Behaviour
By default, when pressing the next or previous buttons, pagination will advance by a single month. This behavior can be changed to paginate by the number of visible months instead, by setting pageBehavior to visible.
import { RangeCalendar } from "@opengovsg/oui"export const Example = () => { return ( <RangeCalendar aria-label="Date (Page Behavior)" visibleDuration={{ months: 2 }} pageBehavior="visible" /> )}Selecting Date on "Today" Button Click
By default, clicking the "Today" button will only move focus to today's date without selecting it. To change this behavior so that clicking the "Today" button also selects today's date, set the shouldSetDateOnTodayButtonClick prop to true.
If there is an open range selection, clicking "Today" will set either the start or end of the range to today's date, depending on which end of the range is already selected.
import { RangeCalendar } from "@opengovsg/oui"export const Example = () => { return ( <RangeCalendar aria-label="Date (Select Date Range on Today Button Click)" shouldSetDateOnTodayButtonClick /> )}Hiding "Today" Button
The "Today" button can be hidden by setting the showTodayButton prop to false.
import { RangeCalendar } from "@opengovsg/oui"export const Example = () => { return ( <RangeCalendar aria-label="Date (Hide Today Button)" showTodayButton={false} /> )}Slots
- base: Calendar wrapper, it handles alignment, placement, and general appearance.
- prevButton: The previous button of the calendar.
- nextButton: The next button of the calendar.
- buttonGroup: The wrapping element of the next and previous buttons of the calendar.
- header: The header element.
- title: A description of the visible date range, for use in the calendar title.
- gridWrapper: The wrapper for the calendar grid.
- grid: The date grid element (e.g.
<table>). - gridHeader: The date grid header element (e.g.
<th>). - gridHeaderCell: The date grid header cell element (e.g.
<td>). - gridBody: The date grid body element (e.g.
<tbody>). - cell: The date grid cell element (e.g.
<td>). - cellButton: The button element within the cell.
- yearSelector: The wrapper for the year picker
- monthSelector: The wrapper for the month picker
- selectors: The wrapping element for the year and month pickers
- monthList: The month list picker.
- yearList: The year list picker.
- selectorText: The text of the selected month or year.
- bottomContentWrapper: The wrapping element for the bottom content.
- todayButton: The "Today" button.
Data Attributes
RangeCalendar has the following attributes on the CalendarCell element (that you can use with Tailwind to style the cells):
data-focused: Whether the cell is focused.data-hovered: Whether the cell is currently hovered with a mouse.data-pressed: Whether the cell is currently being pressed.data-unavailable: Whether the cell is unavailable, according to the calendar'sisDateUnavailableprop. Unavailable dates remain focusable, but cannot be selected by the user. They should be displayed with a visual affordance to indicate they are unavailable, such as a different color or a strikethrough.data-disabled: Whether the cell is disabled, according to the calendar'sminValue,maxValue, andisDisabledprops.data-focus-visible: Whether the cell is keyboard focused.data-outside-visible-range: Whether the cell is outside the visible range of the calendar.data-outside-month: Whether the cell is outside the current month.data-selected: Whether the cell is selected.data-selected-start: Whether the cell is the first date in a range selection.data-selected-end: Whether the cell is the last date in a range selection.data-invalid: Whether the cell is part of an invalid selection.data-highlighted: Whether the cell needs any special highlight. Defaults to the initialdefaultFocusedValueprop (or today's date).data-range-start: Whether the cell is the first day of the week.data-range-end: Whether the cell is the last day of the week.