Date & time
Calendar
A date grid is deceptively hard: keyword navigation, locale week starts, unavailable days that stay focusable, and year jumps that do not require 120 clicks. React Aria owns the grid semantics; LUC owns every pixel of it.
Examples
Single date
Values are `CalendarDate` — a calendar date with no clock and no time zone, so 2026-09-18 is that day everywhere.
Week starts, weekday names and month names follow it.
The heading, weekday row, segment order and numerals all come from the locale — nothing about the month grid is hard-coded to Sunday-start or to Latin digits.
<Calendar
aria-label="Review date"
locale={locale}
value={date}
onValueChange={setDate}
footer={
<CalendarFooter>
<Text variant="caption" tone="muted">{formatDateOnly(date, { locale })}</Text>
<CalendarActionButton onClick={() => setDate(DEMO_TODAY)}>Today</CalendarActionButton>
</CalendarFooter>
}
/>Date range
One month on narrow screens, two on desktop. Both ends are inclusive, and the band is a continuous span rather than a row of pills.
Sep 8 – Sep 18, 2026
<RangeCalendar aria-label="Review period" locale={locale} value={range} onValueChange={setRange} />
<RangeCalendar aria-label="Wide review period" locale={locale} months={2} value={range} onValueChange={setRange} />Unavailable dates
A blocked day stays focusable and stays readable — it is struck through and underlined by a diagonal line, not just dimmed. Selection skips it rather than trapping the user.
<RangeCalendar
aria-label="Period with a blackout"
locale="en-US"
months={2}
value={range}
onValueChange={setRange}
isDateUnavailable={isBlackout}
footer={<CalendarFooter>21–24 September is a blackout window.</CalendarFooter>}
/>Month and year selection
Both are ARIA grids with roving focus: arrow keys move, Home/End jump within a row, Enter selects. Years page by decade, so 1998 is four clicks away from 2026.
MonthPicker
YearPicker
Selected month and year
month: May 2026
year: 2024
The same two components power the calendar header's month/year jump — the component you use is the component the header uses.
<MonthPicker aria-label="Reporting month" locale={locale} value={month} onValueChange={setMonth} maxValue={DEMO_TODAY} />
<YearPicker aria-label="Reporting year" locale={locale} value={year} onValueChange={setYear} />Inside a locale provider
`LocaleProvider` sets the locale for every date and number control underneath, which is also how a server-rendered app avoids a hydration mismatch: pass the locale explicitly instead of relying on `navigator.language`.
Formatted: 18. Sept. 2026
<LocaleProvider locale="de-DE">
<Calendar aria-label="Kalender" value={date} onValueChange={setDate} />
</LocaleProvider>Usage
import { Calendar } from "@luc/ui";<Calendar aria-label="Review date" value={date} onValueChange={setDate} />The value is a `CalendarDate` and `onValueChange` reports one or `null`. A calendar with no popover of its own is what you want inside a page section; wrap `DatePicker` around the same grid when the field should be a form control.
API
Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.
| Export | Kind | Signature |
|---|---|---|
Calendar | 60 documented props | <T extends DateValue = CalendarDate>({ locale, viewSwitch, onValueChange, footer, className, ...props }: CalendarProps<T>) => import("@types/react").JSX.Element |
CalendarActionButton | export | ({ className, type, ...props }: React.ComponentPropsWithRef<"button">) => import("@types/react").JSX.Element |
CalendarFooter | 1 documented props | ({ className, ...props }: CalendarFooterProps) => import("@types/react").JSX.Element |
RangeCalendar | 60 documented props | <T extends DateValue = CalendarDate>({ locale, viewSwitch, months, onValueChange, footer, className, ...props }: RangeCalendarProps<T>) => import("@types/react").JSX.Element |
| Export | Kind | Signature |
|---|---|---|
LocaleProvider | 2 documented props | ({ locale, children }: LocaleProviderProps) => import("@types/react").JSX.Element |
| Export | Kind | Signature |
|---|---|---|
Chevron | 1 documented props | ({ direction }: { direction: "left" | "right" | "down"; }) => import("@types/react").JSX.Element |
MonthPicker | 13 documented props | ({ value, onValueChange, year, defaultYear, onYearChange, minValue, maxValue, isMonthUnavailable, locale, withYearNavigation, footer, className, ...props }: MonthPickerProps) => import("@types/react"… |
YearPicker | 11 documented props | ({ value, onValueChange, pageStart, defaultPageStart, onPageStartChange, yearsPerPage, minValue, maxValue, locale, className, ...props }: YearPickerProps) => import("@types/react").JSX.Element |
Calendar props
Type: CalendarProps<T>
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
aria-describedby?inherited | string | undefined | — | Identifies the element (or elements) that describes the object. |
aria-details?inherited | string | undefined | — | Identifies the element (or elements) that provide a detailed, extended description for the object. |
aria-label?inherited | string | undefined | — | Defines a string value that labels the current element. |
aria-labelledby?inherited | string | undefined | — | Identifies the element (or elements) that labels the current element. |
autoFocus?inherited | boolean | undefined | | Whether to automatically focus the calendar when it mounts. |
className? | string | undefined | — | |
createCalendar?inherited | ((identifier: CalendarIdentifier) => ICalendar) | undefined | — | A function to create a new [Calendar](https://react-spectrum.adobe.com/internationalized/date/Calendar.html) object for a given calendar identifier. If not provided, the `createCa… |
defaultFocusedValue?inherited | DateValue | null | undefined | — | The date that is focused when the calendar first mounts (uncontrolled). |
defaultValue?inherited | T | null | undefined | — | The default value (uncontrolled). |
dir?inherited | string | undefined | — | |
firstDayOfWeek?inherited | "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | undefined | — | The day that starts the week. |
focusedValue?inherited | DateValue | null | undefined | — | Controls the currently focused date within the calendar. |
footer? | ReactNode | — | Rendered under the grid — quick actions belong here, not in the grid. |
hidden?inherited | boolean | undefined | — | |
id?inherited | string | undefined | — | The element's unique identifier. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id). |
inert?inherited | boolean | undefined | — | |
isDateUnavailable?inherited | ((date: DateValue) => boolean) | undefined | — | Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. |
isDisabled?inherited | boolean | undefined | | Whether the calendar is disabled. |
isInvalid?inherited | boolean | undefined | — | Whether the current selection is invalid according to application logic. |
isReadOnly?inherited | boolean | undefined | | Whether the calendar value is immutable. |
lang?inherited | string | undefined | — | |
locale? | string | undefined | — | BCP-47 locale. Pass it to keep server and client output identical. |
maxValue?inherited | DateValue | null | undefined | — | The maximum allowed date that a user may select. |
minValue?inherited | DateValue | null | undefined | — | The minimum allowed date that a user may select. |
onAnimationEnd?inherited | AnimationEventHandler<HTMLDivElement> | undefined | — | |
onAnimationEndCapture?inherited | AnimationEventHandler<HTMLDivElement> | undefined | — | |
onAnimationIteration?inherited | AnimationEventHandler<HTMLDivElement> | undefined | — | |
onAnimationIterationCapture?inherited | AnimationEventHandler<HTMLDivElement> | undefined | — | |
onAnimationStart?inherited | AnimationEventHandler<HTMLDivElement> | undefined | — | |
onAnimationStartCapture?inherited | AnimationEventHandler<HTMLDivElement> | undefined | — | |
onAuxClick?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onAuxClickCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onChange?inherited | ((value: MappedDateValue<T>) => void) | undefined | — | Handler that is called when the value changes. |
onClick?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onClickCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onContextMenu?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onContextMenuCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onDoubleClick?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onDoubleClickCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onFocusChange?inherited | ((date: CalendarDate) => void) | undefined | — | Handler that is called when the focused date changes. |
onGotPointerCapture?inherited | PointerEventHandler<HTMLDivElement> | undefined | — | |
onGotPointerCaptureCapture?inherited | PointerEventHandler<HTMLDivElement> | undefined | — | |
onLostPointerCapture?inherited | PointerEventHandler<HTMLDivElement> | undefined | — | |
onLostPointerCaptureCapture?inherited | PointerEventHandler<HTMLDivElement> | undefined | — | |
onMouseDown?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseDownCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseEnter?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseLeave?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseMove?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseMoveCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseOut?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseOutCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseOver?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseOverCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseUp?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseUpCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onPointerCancel?inherited | PointerEventHandler<HTMLDivElement> | undefined | — | |
onPointerCancelCapture?inherited | PointerEventHandler<HTMLDivElement> | undefined | — | |
onPointerDown?inherited | PointerEventHandler<HTMLDivElement> | undefined | — | |
onPointerDownCapture?inherited | PointerEventHandler<HTMLDivElement> | undefined | — |
LocaleProvider props
Type: LocaleProviderProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
locale? | string | undefined | — | BCP-47 locale, e.g. `en-GB`, `de-DE`, `ja-JP`, `ar-EG`. Date and number controls follow the locale for segment order, weekday names, the first day of the week, the 12/24-hour cloc… |
MonthPicker props
Type: MonthPickerProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
aria-label? | string | undefined | — | |
className? | string | undefined | — | |
defaultYear? | number | undefined | — | |
footer? | ReactNode | — | Rendered under the grid — a "today" or "clear" affordance. |
isMonthUnavailable? | ((firstDayOfMonth: CalendarDate) => boolean) | undefined | — | A month is unavailable when its first day is. |
locale? | string | undefined | — | |
maxValue? | DateValue | null | undefined | — | |
minValue? | DateValue | null | undefined | — | |
onValueChange? | ((value: CalendarDate) => void) | undefined | — | |
onYearChange? | ((year: number) => void) | undefined | — | |
value? | CalendarDate | null | undefined | — | Selected month. The day is ignored. |
withYearNavigation? | boolean | undefined | — | Year navigation row. Hidden inside a calendar that already has one. |
year? | number | undefined | — | Year on display. Controlled or `defaultYear`. |
CalendarFooter props1 props
CalendarFooter props
Type: CalendarFooterProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — |
RangeCalendar props60 props
RangeCalendar props
Type: RangeCalendarProps<T>
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
allowsNonContiguousRanges?inherited | boolean | undefined | — | When combined with `isDateUnavailable`, determines whether non-contiguous ranges, i.e. ranges containing unavailable dates, may be selected. |
aria-describedby?inherited | string | undefined | — | Identifies the element (or elements) that describes the object. |
aria-details?inherited | string | undefined | — | Identifies the element (or elements) that provide a detailed, extended description for the object. |
aria-label?inherited | string | undefined | — | Defines a string value that labels the current element. |
aria-labelledby?inherited | string | undefined | — | Identifies the element (or elements) that labels the current element. |
autoFocus?inherited | boolean | undefined | | Whether to automatically focus the calendar when it mounts. |
className? | string | undefined | — | |
commitBehavior?inherited | "select" | "reset" | "clear" | undefined | | Controls the behavior when a pointer is released outside the calendar or a blur occurs mid selection: - `clear`: clear the currently selected range of dates. - `reset`: reset the … |
createCalendar?inherited | ((identifier: CalendarIdentifier) => ICalendar) | undefined | — | A function to create a new [Calendar](https://react-spectrum.adobe.com/internationalized/date/Calendar.html) object for a given calendar identifier. If not provided, the `createCa… |
defaultFocusedValue?inherited | DateValue | null | undefined | — | The date that is focused when the calendar first mounts (uncontrolled). |
defaultValue?inherited | RangeValue<T> | null | undefined | — | The default value (uncontrolled). |
dir?inherited | string | undefined | — | |
firstDayOfWeek?inherited | "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | undefined | — | The day that starts the week. |
focusedValue?inherited | DateValue | null | undefined | — | Controls the currently focused date within the calendar. |
footer? | ReactNode | — | |
hidden?inherited | boolean | undefined | — | |
id?inherited | string | undefined | — | The element's unique identifier. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id). |
inert?inherited | boolean | undefined | — | |
isDateUnavailable?inherited | ((date: DateValue, anchorDate: CalendarDate | null) => boolean) | undefined | — | Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. The second argument provides the current selection anchor date, if any. Th… |
isDisabled?inherited | boolean | undefined | | Whether the calendar is disabled. |
isInvalid?inherited | boolean | undefined | — | Whether the current selection is invalid according to application logic. |
isReadOnly?inherited | boolean | undefined | | Whether the calendar value is immutable. |
lang?inherited | string | undefined | — | |
locale? | string | undefined | — | |
maxValue?inherited | DateValue | null | undefined | — | The maximum allowed date that a user may select. |
minValue?inherited | DateValue | null | undefined | — | The minimum allowed date that a user may select. |
months? | 1 | 2 | undefined | — | How many months are visible. One on narrow screens, two on desktop — the Showcase switches this with a media query rather than forcing two everywhere. |
onAnimationEnd?inherited | AnimationEventHandler<HTMLDivElement> | undefined | — | |
onAnimationEndCapture?inherited | AnimationEventHandler<HTMLDivElement> | undefined | — | |
onAnimationIteration?inherited | AnimationEventHandler<HTMLDivElement> | undefined | — | |
onAnimationIterationCapture?inherited | AnimationEventHandler<HTMLDivElement> | undefined | — | |
onAnimationStart?inherited | AnimationEventHandler<HTMLDivElement> | undefined | — | |
onAnimationStartCapture?inherited | AnimationEventHandler<HTMLDivElement> | undefined | — | |
onAuxClick?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onAuxClickCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onChange?inherited | ((value: RangeValue<MappedDateValue<T>>) => void) | undefined | — | Handler that is called when the value changes. |
onClick?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onClickCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onContextMenu?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onContextMenuCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onDoubleClick?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onDoubleClickCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onFocusChange?inherited | ((date: CalendarDate) => void) | undefined | — | Handler that is called when the focused date changes. |
onGotPointerCapture?inherited | PointerEventHandler<HTMLDivElement> | undefined | — | |
onGotPointerCaptureCapture?inherited | PointerEventHandler<HTMLDivElement> | undefined | — | |
onLostPointerCapture?inherited | PointerEventHandler<HTMLDivElement> | undefined | — | |
onLostPointerCaptureCapture?inherited | PointerEventHandler<HTMLDivElement> | undefined | — | |
onMouseDown?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseDownCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseEnter?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseLeave?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseMove?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseMoveCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseOut?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseOutCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseOver?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseOverCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseUp?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onMouseUpCapture?inherited | MouseEventHandler<HTMLDivElement> | undefined | — | |
onPointerCancel?inherited | PointerEventHandler<HTMLDivElement> | undefined | — |
Chevron props1 props
Chevron props
Type: { direction: "left" | "right" | "down"; }
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
direction | "right" | "left" | "down" | — |
YearPicker props11 props
YearPicker props
Type: YearPickerProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
aria-label? | string | undefined | — | |
className? | string | undefined | — | |
defaultPageStart? | number | undefined | — | |
locale? | string | undefined | — | |
maxValue? | DateValue | null | undefined | — | |
minValue? | DateValue | null | undefined | — | |
onPageStartChange? | ((year: number) => void) | undefined | — | |
onValueChange? | ((value: CalendarDate) => void) | undefined | — | |
pageStart? | number | undefined | — | First year of the visible page. |
value? | CalendarDate | null | undefined | — | |
yearsPerPage? | number | undefined | — | Years per page. Default 10, aligned to the decade — "2020–2029". |
Guidelines
Do
- Reach for `Calendar` when the grid is the content of the page — a review screen, a scheduler — and for `DatePicker` when the grid belongs behind a field in a form.
- Pass `locale` (or wrap the subtree in `LocaleProvider`) instead of letting the browser decide: that is what keeps week start, month names and numerals identical on the server and in the browser.
- Block a day with `isDateUnavailable` rather than removing it, so the day stays focusable, announced and visible as a struck-through cell.
Don't
- Turn a date-only value into a JavaScript `Date`. `new Date("2026-09-18")` is UTC midnight, so west of Greenwich it reads back as the 17th; `dateOnlyFromInstant(instant, timeZone)` is the one way across, and it demands the zone.
- Force `months={2}` into a narrow column: two months are 472 px wide, which is wider than a phone and clips the second grid.
- Use a date grid to collect a time as well — that is `DateTimePicker`, and a calendar that quietly picked 00:00 would be inventing an answer.
Accessibility
React Aria owns the grid contract: one tab stop for the whole grid, arrow keys to move by day, Page Up and Page Down by month, Home and End to the ends of a week, and each cell announced with its full date and whether it is unavailable. A `Calendar` needs its own accessible name (`aria-label`), and a `RangeCalendar` announces the two ends as start and end rather than as two unrelated grids.
Related