Skip to Content
Puzzle

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.

390 × 600

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.

390 × 620

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.

390 × 600

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.

390 × 700

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.

390 × 760

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.

390 × 650

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.

390 × 720

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.

390 × 680

API

Selection and field props

PropTypeDefaultDescription
valueDate | null-Controlled selection; null represents no date.
defaultValueDate | nullnullInitial uncontrolled selection.
onChange(value: Date | null) => void-Selected local-midnight date, or null when cleared.
labelReactNode-Visible field label.
placeholderReactNode"Select date"Trigger content without a selection.
descriptionReactNode-Supporting text below the control.
errorReactNode | boolean-Error message; true uses "Invalid date".
disabledbooleanfalseDisables the control and native input.
requiredbooleanfalseEnables required validation and prevents clearing.
clearablebooleanfalseAdds a clear action for a selected, non-required date.
clearLabelstring"Clear date"Accessible name of the clear button.
namestring-Native form field name.
formstring-ID of the associated form.
sizeResponsiveValue<DatePickerSize>"md"Trigger size: "sm", "md", or "lg".
fullWidthResponsiveValue<boolean>falseFills the available width.

ResponsiveValue<T> accepts a value or an object with mobile, tablet, and desktop keys.

Availability and localization

PropTypeDefaultDescription
minDateDate-Earliest selectable date, inclusive.
maxDateDate-Latest selectable date, inclusive.
disabledDatesreadonly Date[][]Dates excluded from selection.
shouldDisableDate(date: Date) => boolean-Returns true for unavailable dates.
localestringRuntime localeLocale for dates, month names, weekdays, and numbers.
weekStartsOn0 | 1 | 2 | 3 | 4 | 5 | 6Locale week startFirst weekday; Sunday is 0. Sunday is the fallback when locale week information is unavailable.
dateFormatIntl.DateTimeFormatOptionsLong month, numeric day/yearFormat for the selected date in the trigger.
calendarLabelstring"Choose date"Accessible name of the calendar dialog.
previousMonthLabelstring"Previous month"Previous navigation label prefix; target month is appended.
nextMonthLabelstring"Next month"Next navigation label prefix; target month is appended.
PropTypeDefaultDescription
openboolean-Controlled calendar visibility.
defaultOpenbooleanfalseInitial 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.
offsetnumber8Trigger-to-popup distance in pixels.
collisionPaddingnumber8Minimum viewport-edge distance in pixels.
classNamestring-Additional class on the trigger button.
rootClassNamestring-Additional class on the outer field.
popoverClassNamestring-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".

KeyBehavior
Enter, Space, Up/Down on the triggerOpens the calendar and focuses an available day.
Left/RightMoves one day, skipping unavailable dates; horizontal direction follows RTL.
Up/Down in the calendarMoves one week, then searches for an available date.
Home/EndMoves to the first/last available day in the current week.
PageUp/PageDownMoves one month, clamping the day at month boundaries.
Shift + PageUp/PageDownMoves one year, including leap-day clamping.
Enter/Space on a daySelects it, closes the calendar, and returns focus to the trigger.
EscapeCloses the calendar and returns focus to the trigger.
Tab/Shift + TabCloses 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.

;