Date Picker
Choose a single date from an accessible calendar popup.
Date Picker combines a button trigger, a calendar dialog, and a native date input for form integration. It selects a single calendar date, not a time or a date range.
Import
import {
DatePicker,
type DatePickerProps,
type DatePickerSize,
} from "@puzzle-ui/ui";Basic usage
Use label for a visible, accessible field name. placeholder appears when nothing is selected, and description provides supporting text. Click the trigger or use Enter, Space, ArrowUp, or ArrowDown to open the calendar.
The calendar opens at the selected date when it is available. Without an available selection, it starts from today or the nearest available date within your bounds. Selecting a day closes the calendar and returns focus to the trigger. Today selects the current date when it is available.
Date values and time zones
Pass JavaScript Date objects. Date Picker normalizes them to midnight in the local time zone and calls onChange with a new Date instance, or null when cleared.
Use new Date(year, monthIndex, day) for calendar dates; months are zero-based. For example, new Date(2025, 2, 15) is 15 March 2025. Avoid new Date("2025-03-15") for a local date because date-only ISO strings are parsed as UTC and can display a different day in some time zones.
Controlled selection and clearing
Use value with onChange to own the selection. Date Picker uses null for a controlled empty value; omitting value enables uncontrolled usage with defaultValue.
clearable adds a clear button when a date is selected. Customize its accessible name with clearLabel. Required fields cannot be cleared through this button.
Date availability
minDate and maxDate define inclusive bounds. Use disabledDates for specific exclusions and shouldDisableDate for recurring rules such as weekends. All restrictions are combined; the callback receives a date normalized to local midnight.
Unavailable days remain visible but disabled, and keyboard navigation skips them. Month navigation is disabled at the minimum and maximum boundaries. A selected date that becomes unavailable is not silently replaced; its native input is marked invalid until a valid date is selected.
Locale, week start, and formatting
locale controls month names, weekday labels, day numbers, and the displayed selection. weekStartsOn overrides the locale's first weekday: 0 is Sunday, 1 is Monday, through 6 for Saturday.
dateFormat customizes the trigger's date format without shortening the accessible names of day buttons. Translate calendarLabel, previousMonthLabel, nextMonthLabel, and clearLabel for your application. The built-in Today action and empty-calendar message currently use English text.
Sizes and responsive width
size supports sm, md, and lg. fullWidth fills the available width. Both accept responsive values using mobile, tablet, and desktop keys. Change the preview device to compare the responsive control.
Popup placement and visibility
position requests a side, align controls alignment, and offset sets the trigger gap in pixels. collisionPadding reserves space at viewport edges. The calendar can fall back to another side when the requested placement does not fit.
defaultOpen sets initial uncontrolled visibility. Use open with onOpenChange for controlled visibility, independently of controlled selection.
Required, error, and disabled states
required marks the field required and enables native required validation. error displays a message and marks the trigger invalid; error={true} displays "Invalid date". External aria-invalid is combined with the error state.
disabled prevents interaction and excludes the native input from form submission. Supporting description and error IDs are combined with any external aria-describedby.
Native forms
Supply name to include the selected date in native form submission as YYYY-MM-DD, regardless of the display locale. form associates the input with a form elsewhere in the document. The hidden native input carries required, minimum, maximum, and custom date-availability validation; invalid submission focuses the visible trigger.
API
Selection and field props
| Prop | Type | Default | Description |
|---|---|---|---|
value | Date | null | - | Controlled selection; null represents no date. |
defaultValue | Date | null | null | Initial uncontrolled selection. |
onChange | (value: Date | null) => void | - | Selected local-midnight date, or null when cleared. |
label | ReactNode | - | Visible field label. |
placeholder | ReactNode | "Select date" | Trigger content without a selection. |
description | ReactNode | - | Supporting text below the control. |
error | ReactNode | boolean | - | Error message; true uses "Invalid date". |
disabled | boolean | false | Disables the control and native input. |
required | boolean | false | Enables required validation and prevents clearing. |
clearable | boolean | false | Adds a clear action for a selected, non-required date. |
clearLabel | string | "Clear date" | Accessible name of the clear button. |
name | string | - | Native form field name. |
form | string | - | ID of the associated form. |
size | ResponsiveValue<DatePickerSize> | "md" | Trigger size: "sm", "md", or "lg". |
fullWidth | ResponsiveValue<boolean> | false | Fills the available width. |
ResponsiveValue<T> accepts a value or an object with mobile, tablet, and desktop keys.
Availability and localization
| Prop | Type | Default | Description |
|---|---|---|---|
minDate | Date | - | Earliest selectable date, inclusive. |
maxDate | Date | - | Latest selectable date, inclusive. |
disabledDates | readonly Date[] | [] | Dates excluded from selection. |
shouldDisableDate | (date: Date) => boolean | - | Returns true for unavailable dates. |
locale | string | Runtime locale | Locale for dates, month names, weekdays, and numbers. |
weekStartsOn | 0 | 1 | 2 | 3 | 4 | 5 | 6 | Locale week start | First weekday; Sunday is 0. Sunday is the fallback when locale week information is unavailable. |
dateFormat | Intl.DateTimeFormatOptions | Long month, numeric day/year | Format for the selected date in the trigger. |
calendarLabel | string | "Choose date" | Accessible name of the calendar dialog. |
previousMonthLabel | string | "Previous month" | Previous navigation label prefix; target month is appended. |
nextMonthLabel | string | "Next month" | Next navigation label prefix; target month is appended. |
Popup and styling
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | - | Controlled calendar visibility. |
defaultOpen | boolean | false | Initial uncontrolled visibility. |
onOpenChange | (open: boolean) => void | - | Called when visibility changes are requested. |
position | "top" | "right" | "bottom" | "left" | "bottom" | Preferred popup side. |
align | "start" | "center" | "end" | "start" | Alignment along the trigger. |
offset | number | 8 | Trigger-to-popup distance in pixels. |
collisionPadding | number | 8 | Minimum viewport-edge distance in pixels. |
className | string | - | Additional class on the trigger button. |
rootClassName | string | - | Additional class on the outer field. |
popoverClassName | string | - | Additional class on the popup content. |
Other supported native button attributes, including id, style, dir, aria-label, aria-labelledby, and aria-describedby, apply to the trigger. Use an accessible name when there is no visible label.
Accessibility and keyboard interaction
The trigger exposes aria-haspopup="dialog", aria-expanded, and aria-controls while open. The dialog contains a labelled grid; days have full localized date names, selected cells expose aria-selected, and today exposes aria-current="date".
| Key | Behavior |
|---|---|
| Enter, Space, Up/Down on the trigger | Opens the calendar and focuses an available day. |
| Left/Right | Moves one day, skipping unavailable dates; horizontal direction follows RTL. |
| Up/Down in the calendar | Moves one week, then searches for an available date. |
| Home/End | Moves to the first/last available day in the current week. |
| PageUp/PageDown | Moves one month, clamping the day at month boundaries. |
| Shift + PageUp/PageDown | Moves one year, including leap-day clamping. |
| Enter/Space on a day | Selects it, closes the calendar, and returns focus to the trigger. |
| Escape | Closes the calendar and returns focus to the trigger. |
| Tab/Shift + Tab | Closes the calendar and resumes tab navigation from the trigger. |
Month changes are announced politely. Provide visible explanations for unavailable dates and validation errors rather than relying only on disabled styling.