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.
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.
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.
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.
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.
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.
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.
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.
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.
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 separateuseDateRangePickerState/useDateRangePickerhook 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 —
granularityapplies to single selection;selectionMode="multiple"lists dates only.
API
DatePicker
| Prop | Type | Default |
|---|---|---|
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? | boolean | false |
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? | number | 1 |
| Number of months to display side by side in the calendar overlay. | ||