Skip to main contentSkip to docs navigation

Datepicker

Segmented text field with a calendar popover for entering or picking a date.

Overview

DatePicker combines an individually-editable segmented date field (day/month/year, each a separate keyboard-navigable unit) with a calendar popover — type a date directly or pick one from the grid. Both stay in sync with a single selected value. The popover renders Calendar, which is also available on its own wherever an inline, non-popover calendar is needed.

JSX
import { DatePicker } from '@chassis-ui/react'

export const Example = () => {
  return <DatePicker aria-label="Event date" />
}

As a form field

Pass label/help to wrap the date picker in a .form-field with an associated FormLabel and FormHelp. Pass name to have DatePicker create and manage a hidden <input> for native form submission — its value stays in sync with the selection, formatted as an ISO 8601 date string (2026-07-24), with a time 2026-07-24T09:30:00 (see Time).

With required, an empty date picker holds its form back from submitting: the first segment takes focus and the picker shows as invalid, with its invalidFeedback, until it has a value or the form is reset. invalid, once set, and valid decide how it looks instead. With selectionMode="multiple" it asks for one date at least, and the calendar button takes the focus.

We’ll send a reminder the day before.
JSX
import { DatePicker } from '@chassis-ui/react'

export const Example = () => {
  return (
    <DatePicker label="Event date" help="We’ll send a reminder the day before." name="eventDate" />
  )
}

Min & max dates

Restrict the selectable range with minValue/maxValue. Dates outside the range are shown but disabled in the calendar grid. A value typed outside the range marks the field invalid, as invalid does, and shows invalidFeedback if there is one.

JSX
import { DatePicker } from '@chassis-ui/react'
import { today, getLocalTimeZone } from '@internationalized/date'

export const Example = () => {
  const now = today(getLocalTimeZone())

  return (
    <DatePicker aria-label="Appointment date" maxValue={now.add({ days: 30 })} minValue={now} />
  )
}

Time

Pass granularity as 'hour', 'minute' or 'second' to add a time to the field. Its segments follow the date's in the locale's order and hour cycle (hourCycle sets 12 or 24 hours), and the value becomes a CalendarDateTime. A value that carries a time shows it without granularity; with a time granularity, a value or placeholderValue must carry one (a CalendarDate there throws).

The calendar picks the day and the field's segments edit the time. Picking a day keeps the time already set, so the day changes without retyping it. In an empty field it takes the time of defaultValue, else of placeholderValue, else midnight. minValue/maxValue may carry a time: the calendar compares days only, so their own days stay selectable, and a time outside the range marks the field invalid.

Pick a day, then type the time.
JSX
import { DatePicker } from '@chassis-ui/react'

export const Example = () => {
  return (
    <DatePicker
      granularity="minute"
      help="Pick a day, then type the time."
      label="Appointment"
      name="appointment"
    />
  )
}

A ZonedDateTime as value, defaultValue or placeholderValue adds its time zone to the field (hideTimeZone leaves it out), and onChange receives ZonedDateTimes. A day picked after the field is cleared is built from defaultValue or placeholderValue as above, so a controlled zoned value needs a zoned placeholderValue too, or the next value is a CalendarDateTime with no zone. The hidden input holds the zone too, 2026-11-05T09:30:00+09:00[Asia/Tokyo], which parseZonedDateTime reads back. The calendar shows that zone's days, and marks today by that zone's date.

JSX
import { DatePicker } from '@chassis-ui/react'
import { parseZonedDateTime } from '@internationalized/date'

export const Example = () => {
  return (
    <DatePicker
      defaultValue={parseZonedDateTime('2026-11-05T09:30[Asia/Tokyo]')}
      label="Call with the Tokyo office"
    />
  )
}

Locale & calendar system

DatePicker reads locale from React Aria's I18nProvider (or the browser's locale if none is set). Segment order, weekday names, and the calendar system itself all follow the active locale — this example uses ar-SA, which renders right-to-left and uses the Umm al-Qura calendar instead of Gregorian. See Internationalization for setting a locale and its relationship to RTL layout.

JSX
import { DatePicker, I18nProvider } from '@chassis-ui/react'

// `ar-SA` uses the Umm al-Qura calendar (non-Gregorian) and reads right-to-left — both the
// segment order and the calendar grid adapt automatically, sourced from `@internationalized/date`
// rather than hand-rolled locale logic.
export const Example = () => {
  return (
    <I18nProvider locale="ar-SA">
      <div dir="rtl">
        <DatePicker aria-label="تاريخ الحدث" />
      </div>
    </I18nProvider>
  )
}

Unavailable dates

Pass isDateUnavailable to mark specific dates as unselectable without excluding them from the grid — useful for booked dates or holidays. Unavailable dates remain focusable (so screen reader users can discover why they can't be picked) but can't be selected. This applies to both the calendar popover and typing a date directly into the field.

For data-driven cases, pass unavailableDates instead — a plain array of ISO 8601 (YYYY-MM-DD) strings — as a convenience alternative. It composes with isDateUnavailable when both are given.

Weekend styling

Saturday/Sunday (or whichever days the active locale considers the weekend) are shown muted in the calendar popover automatically — inherited from Calendar, no prop required.

First day of week

The calendar popover's week starts on Monday by default, regardless of the active locale. Pass firstDayOfWeek to override it, same as Calendar.

Multiple dates

Pass selectionMode="multiple" to select any number of independent dates instead of one. A segmented day/month/year field can't represent more than one date, so multiple selection replaces it with a read-only, comma-separated list of the selected dates — the calendar popover still works the same way, just toggling dates on and off instead of replacing a single value. value/defaultValue/onChange switch from a single date to an array of dates, and a name'd hidden input is created per selected date instead of just one.

JSX
import { DatePicker } from '@chassis-ui/react'

export const Example = () => {
  return <DatePicker aria-label="Event dates" selectionMode="multiple" />
}

Multiple months

Pass visibleMonths to show more than one month side by side in the calendar popover — independent of selectionMode, same as on Calendar itself.

JSX
import { DatePicker } from '@chassis-ui/react'

export const Example = () => {
  return <DatePicker aria-label="Event date" visibleMonths={2} />
}

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, DatePicker } from '@chassis-ui/react'

export const Example = () => {
  const [visible, setVisible] = useState(false)

  return (
    <div className="vstack gap-sm">
      <div className="d-flex align-items-center gap-sm">
        <DatePicker aria-label="Event date" 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>
  )
}

Styling

Unlike most Chassis React components, DatePicker's calendar grid isn't styled by @chassis-ui/css — chassis-css's own datepicker styling is built entirely around Vanilla Calendar Pro's specific DOM output, which React Aria's calendar hooks don't produce. The grid itself is styled by Calendar's own scoped CSS (built on @chassis-ui/tokens' public design tokens, so it still matches the current theme and light/dark mode), and DatePicker adds its own for the field's segments and the popover shell, all in @chassis-ui/react/style.css, which an app imports once (see Theming). The field and toggle button, in turn, reuse chassis-css's existing .form-input/.input-adorn/.ghost-input classes as-is.

Scope

The strong-fit case for React Aria's date picker hooks is a single editable date field. A few things are deliberately out of scope for this pass:

  • Date ranges — for a start/end range, see DateRangePicker, built on the separate useDateRangePickerState/useDateRangePicker hook pair.
  • A time picker in the popover — the time is edited in the field's segments, and the popover picks the day only. For a time with no date, use TimeField.
  • A time with multiple dates — granularity applies to single selection; selectionMode="multiple" lists dates only.

API

DatePicker

PropTypeDefault
aria-label?string—
An accessible label for the date picker, used when there's no visible <label>.
aria-labelledby?string—
Identifies a visible <label> element for the date 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?DateValue | DateValue[] | null—
The initial selected date (uncontrolled). The initial selected dates (uncontrolled).
defaultVisible?booleanfalse
Whether the calendar popover is open when the date picker first renders. Use it instead of visible when nothing outside needs to control the popover.
disabled?boolean—
Prevents the date 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 value with a time
The smallest unit the field shows and edits. 'hour', 'minute' and 'second' add a time to the date: the value is then a CalendarDateTime, or a ZonedDateTime when value, defaultValue or placeholderValue is one, and a value or placeholder given must carry a time. Picking a day in the calendar keeps the time. A day picked in an empty field takes the time, and zone, of defaultValue, else placeholderValue, else midnight with no zone: a controlled zoned value, once cleared, needs a zoned placeholderValue to stay zoned.
help?ReactNode—
A description for the field, rendered below the date picker.
hideTimeZone?boolean—
Hide the time zone of a ZonedDateTime value.
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 picker when invalid is set, and after a submit that an empty required date picker 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—
name of an auto-created hidden input, kept in sync with the selection, for native form submission — one input when selectionMode is 'single', one per selected date when it's 'multiple' (same name on each, which browsers serialize as multiple form values). Omit to skip creating any. The 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: DateValue | null) => void) | ((value: DateValue[]) => void)—
Callback fired when the selected date changes. With a time (granularity), it receives a CalendarDateTime or ZonedDateTime. Callback fired when the set of selected dates changes.
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 the segments start from while the field is empty, such as new CalendarDateTime(2026, 1, 1, 9), and the month the calendar opens on. With no defaultValue, its time is the one a day picked in the calendar gets, and its type the type of value onChange receives: a ZonedDateTime gives zoned values. With a time granularity it must carry a time.
required?boolean—
Toggle the required state for the component. An empty required date picker holds its form back from submitting, takes focus and shows as invalid, with invalidFeedback, until it has a value or the form is reset. invalid, once set, and valid decide how it looks instead. With selectionMode="multiple" it asks for one date at least, and the calendar button takes the focus.
selectionMode?"single" | "multiple"'single' 'single'
Whether a single date or multiple, independently toggled dates can be selected. Multiple selection replaces the editable segmented field with a read-only, comma-separated list of the selected dates — a segmented day/month/year field has no way to represent more than one date.
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 the field.
valid?boolean—
Set component validation state to valid.
validFeedback?ReactNode—
A success message for the field, rendered below the date picker when valid is set.
value?DateValue | DateValue[] | null—
The selected date (controlled). The selected dates (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.