Open UI

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.json
pnpm dlx shadcn@latest add https://oui.open.gov.sg/r/calendar.json
npx shadcn@latest add https://oui.open.gov.sg/r/calendar.json
bunx --bun shadcn@latest add https://oui.open.gov.sg/r/calendar.json

Calendar 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 to errorMessage for a static message, or a function (validation: ValidationResult) => string for 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) => boolean that marks individual dates as unavailable (focusable but not selectable).

Events

  • onChange — Called when the selected date changes, receiving the new DateValue (or null when cleared).
  • onFocusChange — Called when the focused date within the grid changes, receiving a DateValue.

Accessibility

  • Built on React Aria's Calendar with full ARIA grid / gridcell semantics 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-disabled or aria-selected="false" applied automatically.
  • If no visible label surrounds the calendar, pass aria-label or aria-labelledby to 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's isDateUnavailable prop. 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's minValue, maxValue, and isDisabled props.
  • 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

PropTypeDefaultDescription
valueDateValue | null-The selected date (controlled)
defaultValueDateValue | null-The default selected date (uncontrolled)
onChange(value: DateValue | null) => void-Called when the selected date changes
isDisabledbooleanfalseWhether the calendar is disabled
isReadOnlybooleanfalseWhether the calendar is read-only
isInvalidbooleanfalseWhether the calendar should display as invalid
errorMessagestring-Error message to display when the calendar is in an invalid state
minValueDateValue-The minimum selectable date; dates before this are disabled
maxValueDateValue-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)
visibleDurationDateDuration-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)
offsetMonthsnumber0Month offset applied to the header of this grid pane when multiple months are displayed
showTodayButtonbooleantrueWhether to show the "Today" button below the calendar grid
shouldSetDateOnTodayButtonClickbooleanfalseIf true, clicking "Today" selects today's date; if false, it only moves focus to today
bottomContentReactNode-Custom content rendered below the calendar grid, replaces the "Today" button when provided
classNamesSlotsToClasses<CalendarSlots>-Custom Tailwind classes for each calendar slot

Inherits all remaining props from React Aria's Calendar. Notable ones: autoFocus, focusedValue, onFocusChange, weekdayStyle.

On this page