Calendar
A calendar displays one or more date grids and allows users to select a single date.
import { parseDate } from "@internationalized/date"import { Calendar } from "@opengovsg/oui"export const Example = () => { return ( <div className="flex gap-x-4"> <Calendar aria-label="Date (No Selection)" /> <Calendar aria-label="Date (Uncontrolled)" defaultValue={parseDate("2020-02-03")} /> </div> )}Usage
Use Calendar for inline single-date selection (no popover). Use DatePicker when the calendar should appear in a popover. Use RangeCalendar for date ranges.
import { Calendar } from "@opengovsg/oui"<Calendar label="Select a date" />Alternatively, install the component as local source via the shadcn CLI:
npx shadcn@latest add https://oui.open.gov.sg/r/calendar.jsonpnpm dlx shadcn@latest add https://oui.open.gov.sg/r/calendar.jsonnpx shadcn@latest add https://oui.open.gov.sg/r/calendar.jsonbunx --bun shadcn@latest add https://oui.open.gov.sg/r/calendar.jsonCalendar provides an inline date grid that users can browse and select from. It is built on React Aria's Calendar. Date values use objects from the @internationalized/date package, which handles correct international date manipulation across calendars, time zones, and localization concerns.
If no visible label is rendered, pass aria-label or aria-labelledby to identify the calendar to assistive technology.
Examples
Disabled
The isDisabled boolean prop makes the Calendar disabled. Cells cannot be focused or selected.
import { Calendar } from "@opengovsg/oui"export const Example = () => { return <Calendar isDisabled aria-label="Disabled state" />}Controlled
A Calendar has no selection by default. An initial, uncontrolled value can be provided to the Calendar using the defaultValue prop. Alternatively, a controlled value can be provided using the value prop.
import React from "react"import { parseDate } from "@internationalized/date"import { Calendar } from "@opengovsg/oui"export const Example = () => { const [value, setValue] = React.useState(parseDate("2025-03-20")) return ( <div className="flex flex-col gap-4"> <Calendar aria-label="Date (Controlled)" value={value} onChange={setValue} /> <div>Selected date: {value.toString()}</div> </div> )}Min Date Value
The minValue can be used to prevent the user from selecting dates outside a certain range.
By default, Calendar 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 { Calendar } from "@opengovsg/oui"export const Example = () => { return ( <Calendar aria-label="Date (Min Date Value)" defaultValue={today(getLocalTimeZone())} 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, Calendar allows selecting any date between 1 Jan 1900 and 31 December 2100. Override this with the minValue prop.
This example only accepts dates before today.
import { getLocalTimeZone, today } from "@internationalized/date"import { Calendar } from "@opengovsg/oui"export const Example = () => { return ( <Calendar aria-label="Date (Max Date Value)" defaultValue={today(getLocalTimeZone())} maxValue={today(getLocalTimeZone())} /> )}Unavailable Dates
Calendar 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:
<Calendar
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 { Calendar } 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 ( <Calendar aria-label="Date (Unavailable)" isDateUnavailable={isDateUnavailable} /> )}International Calendars
Calendar 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 { Calendar } from "@opengovsg/oui"export const Example = () => { return ( <I18nProvider locale="zh-SG"> <Calendar aria-label="Date (International Calendar)" /> </I18nProvider> )}Visible Duration
By default, the Calendar 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 { Calendar } from "@opengovsg/oui"export const Example = () => { return ( <Calendar 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 { Calendar } from "@opengovsg/oui"export const Example = () => { return <Calendar 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 { Calendar } from "@opengovsg/oui"export const Example = () => { return ( <Calendar 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.
import { Calendar } from "@opengovsg/oui"export const Example = () => { return ( <Calendar aria-label="Date (Select Date on Today Button Click)" shouldSetDateOnTodayButtonClick /> )}Hiding "Today" Button
The "Today" button can be hidden by setting the showTodayButton prop to false.
import { Calendar } from "@opengovsg/oui"export const Example = () => { return ( <Calendar aria-label="Date (Hide Today Button)" showTodayButton={false} /> )}Validation
See the Forms guide for end-to-end patterns including React Hook Form + Zod.
isInvalid+errorMessage— Use together to display an error state. Pass a string toerrorMessagefor a static message, or a function(validation: ValidationResult) => stringfor dynamic messages based on React Aria's validation result.minValue/maxValue— Constrain the selectable date range; dates outside the range are displayed as disabled.isDateUnavailable— A callback(date: DateValue) => booleanthat marks individual dates as unavailable (focusable but not selectable).
Events
onChange— Called when the selected date changes, receiving the newDateValue(ornullwhen cleared).onFocusChange— Called when the focused date within the grid changes, receiving aDateValue.
Accessibility
- Built on React Aria's Calendar with full ARIA
grid/gridcellsemantics for the date grid. - The grid is keyboard navigable: Arrow keys to move focus, Enter or Space to select, Page Up / Page Down to change months, Home / End to jump to the start or end of a week.
- Unavailable and disabled cells remain focusable but cannot be selected, with
aria-disabledoraria-selected="false"applied automatically. - If no visible label surrounds the calendar, pass
aria-labeloraria-labelledbyto identify it to assistive technology. - The visible date range title is announced via a
<h2>inside the calendar header.
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: 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
Calendar 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.
Props
Calendar
| Prop | Type | Default | Description |
|---|---|---|---|
value | DateValue | null | - | The selected date (controlled) |
defaultValue | DateValue | null | - | The default selected date (uncontrolled) |
onChange | (value: DateValue | null) => void | - | Called when the selected date changes |
isDisabled | boolean | false | Whether the calendar is disabled |
isReadOnly | boolean | false | Whether the calendar is read-only |
isInvalid | boolean | false | Whether the calendar should display as invalid |
errorMessage | string | - | Error message to display when the calendar is in an invalid state |
minValue | DateValue | - | The minimum selectable date; dates before this are disabled |
maxValue | DateValue | - | The maximum selectable date; dates after this are disabled |
isDateUnavailable | (date: DateValue) => boolean | - | A function that returns true for dates that should be unavailable (focusable but not selectable) |
visibleDuration | DateDuration | - | The number of months (or days/weeks) displayed at once. Going above 3 months is not recommended |
pageBehavior | "single" | "visible" | - | Whether paginating advances by one month ("single") or by the full visible range ("visible") |
firstDayOfWeek | "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | - | Override the first day of the week (defaults to locale) |
offsetMonths | number | 0 | Month offset applied to the header of this grid pane when multiple months are displayed |
showTodayButton | boolean | true | Whether to show the "Today" button below the calendar grid |
shouldSetDateOnTodayButtonClick | boolean | false | If true, clicking "Today" selects today's date; if false, it only moves focus to today |
bottomContent | ReactNode | - | Custom content rendered below the calendar grid, replaces the "Today" button when provided |
classNames | SlotsToClasses<CalendarSlots> | - | Custom Tailwind classes for each calendar slot |
Inherits all remaining props from React Aria's Calendar. Notable ones: autoFocus, focusedValue, onFocusChange, weekdayStyle.