Skip to main content
On this pageOverview

Date Picker

Overview

An accessible date picker that wraps Calendar in a Popover. Consumers provide the trigger button face and the calendar grid layout. DatePicker handles focus choreography (opening focuses the grid, closing returns focus to the trigger), open/close state, and an optional hidden form input for native form submission.

State Ownership

DatePicker is a Submodel. Initialize it with DatePicker.init(), store its Model in the parent, wire its Messages through Update.foldChild, and render it with DatePicker.view().

The parent owns the selected date. Store it in the parent Model, pass it to the view as maybeSelectedDate, and handle SelectedDate and ClearedDate in the fold's foldOutMessage. DatePicker can also emit ChangedViewMonth when navigation changes the visible month.

Programmatic Control

For programmatic control in a parent update, fold DatePicker.open and DatePicker.close with Update.foldChildStep. Both helpers can emit an OutMessage that reports whether the DatePicker opened or closed.

Calendar Views

The calendar heading inside the popover is a button: clicking it switches the day grid into a 3x4 months grid; clicking the year heading from there switches into a paged 3x4 years grid. Selecting a year drills back to the months grid for that year; selecting a month drills back to the days grid for that month. Re-opening the popover always shows the day grid.

See it in an app

Check out how DatePicker is wired up in a real Foldkit app.

Examples

A date picker constrained to a one-year window around today via minDate and maxDate. Click the trigger to open, pick a date, click the heading to drill into a months grid (and again to drill into a years grid), or navigate with the full WAI-ARIA grid keyboard pattern. Press Enter to commit, Escape to dismiss.

Date picker

Styling

DatePicker is headless. You control the trigger button via triggerContent and triggerClassName, the popover panel via panelClassName, and the calendar grid via the toCalendarView callback. Data attributes on day cells let you style state variants with CSS selectors like group-data-[selected]: and group-data-[disabled]:.

AttributeCondition
data-todayPresent on the cell representing "today". The day cell in Days mode, the current month cell in Months mode, the current year cell in Years mode.
data-selectedPresent on the calendar's currently-centered cell. The selected date in Days mode, the centered month (viewMonth) in Months mode, the centered year (viewYear) in Years mode.
data-focusedPresent on the cell at the keyboard cursor position while the grid has DOM focus.
data-outside-month(Days mode only.) Present on cells that fall outside the currently-viewed month (leading/trailing grid rows).
data-disabledPresent on cells disabled by min/max, disabledDaysOfWeek, or disabledDates.
data-openPresent on the trigger button and wrapper while the popover is open.
data-placementPresent on the calendar panel, set to the side it currently sits on: top, right, bottom, or left. Fixed to the first resolved side when isPlacementLocked is true.

Keyboard Interaction

The trigger button opens the popover on Enter, Space, or ArrowDown. Inside the popover, the calendar grid handles the full WAI-ARIA grid keyboard pattern. Escape closes the popover from both the trigger and the grid.

KeyDescription
Enter / Space / ArrowDownOpen the popover when the trigger button is focused.
EscapeClose the popover from the trigger button or from inside the calendar grid.
ArrowLeft / ArrowRightMove the focus cursor by one cell. Days: ±1 day. Months: ±1 month (wraps across years). Years: ±1 year (wraps across pages).
ArrowUp / ArrowDownMove the focus cursor by one row. Days: ±1 week (7 days). Months: ±1 row (3 months). Years: ±1 row (3 years).
Home / End(Days mode only.) Move focus to the start / end of the current week (based on locale.firstDayOfWeek).
PageUp / PageDownDays: ±1 month. Months: ±1 year. Years: ±1 window (12 years).
Shift + PageUp / Shift + PageDown(Days mode only.) Move focus by one year.
Enter / SpaceCommit the focus cursor. Days: select the date and close the popover. Months: jump the calendar to that month and drill back to Days. Years: jump to that year and drill back to Months.

Accessibility

The trigger button uses aria-expanded and aria-controls to announce the popover relationship. Inside the popover, the calendar grid renders with role="grid" and an explicit aria-label that leads with a non-numeric word ("Calendar, April 2026") so VoiceOver does not pattern-match the grid's row position into a date literal. aria-activedescendant tracks the keyboard cursor; rows carry role="row" with aria-rowindex; cells carry role="gridcell", aria-colindex, and aria-selected on the chosen date. Day buttons carry full accessible names via aria-label and disabled days get aria-disabled="true". When a hidden form input is enabled via the name prop, the selected date is encoded as an ISO string (YYYY-MM-DD) for native form submission.

Naming the Trigger

Give the trigger an accessible name. For a visible label, wire a native <label for> that targets the trigger id with DatePicker.triggerId(id) rather than hardcoding the -popover-button convention. The for association makes the trigger properly labeled: assistive technology announces it by the visible label text, and clicking the label opens the date picker. That is why it is the recommended pattern.

Two ViewInputs fields cover the cases a <label for> does not. Pass ariaLabel for an icon-only trigger with no visible label, or ariaLabelledBy when the element that names the trigger is not a <label> you can point for at.

API Reference

InitConfig

Configuration object passed to DatePicker.init(). Calendar constraints (min/max, disabled dates) are forwarded to the embedded Calendar submodel.

NameTypeDefaultDescription
idstring—Unique ID for the date picker instance.
todayCalendarDate—The current calendar date. Typically fetched at the app boundary via Calendar.today.local and threaded through flags.
initialViewDateCalendarDate—Seeds the month the calendar opens onto. When set, the view starts on the month containing this date. The parent owns the selection itself; pass its current value here to open onto that month.
isAnimatedbooleanfalseEnables animation coordination on the popover panel (enter/leave animations).
localeLocaleConfigdefaultEnglishLocaleMonth and day names, the first day of the week, and the DateFormat for each shape the calendar renders. Import from foldkit/calendar.
minDateCalendarDate—Earliest selectable date. Dates before minDate are marked disabled and skipped by keyboard navigation.
maxDateCalendarDate—Latest selectable date. Dates after maxDate are marked disabled and skipped by keyboard navigation.
disabledDaysOfWeekReadonlyArray<DayOfWeek>[]Days of the week to disable (e.g. ["Saturday", "Sunday"] for weekday-only selection).
disabledDatesReadonlyArray<CalendarDate>[]Explicit list of disabled dates (e.g. holidays). Pre-compute for complex rules.

Model

The DatePicker Model. Stored on your parent Model and threaded through DatePicker.update() and DatePicker.view().

NameTypeDefaultDescription
idstring—The date picker instance ID.
calendarCalendar.Model—The embedded Calendar submodel. Forwards navigation, focus, locale, and disabled-cell state. The picker delegates Calendar messages and resets the calendar to Days mode every time the popover opens or closes.
popoverPopover.Model—The embedded Popover submodel. Tracks open/close state, animation phase, and focus choreography (opening focuses the calendar grid, closing returns focus to the trigger).

ViewInputs

Pass these fields under viewInputs when h.submodel renders DatePicker.view. The surrounding h.submodel configuration separately receives slotId, model, view, and toParentMessage.

NameTypeDefaultDescription
maybeSelectedDateOption<CalendarDate>—The parent-owned selected date. Passed through to the calendar (selected-day marker), the trigger content, and the hidden form input. The picker does not store the selection itself; fold the SelectedDate and ClearedDate OutMessages into this field and pass it back on every render.
anchorAnchorConfig—Popover positioning config (placement, gap, offset, padding, isPlacementLocked, and portal). Controls where the calendar panel floats relative to the trigger. Portaled to the document body, or into an enclosing dialog, by default; pass portal: false to keep the panel inside its wrapper.
triggerContent(maybeDate: Option<CalendarDate>) => Html—Renders the trigger button face. Receives the current selection so you can show the formatted date or a placeholder.
toCalendarView(attributes: CalendarAttributes) => Html—Renders the calendar grid layout inside the popover panel. Same callback shape as Calendar.view toView. Lay out the attribute groups (for example grid, header, weeks, or cells) however you like.
isDisabledbooleanfalseDisables the trigger button, preventing the popover from opening.
namestring—When provided, renders a hidden <input> with this name and the selected date encoded as an ISO string (YYYY-MM-DD) for native form submission.
triggerClassName / triggerAttributesstring / ReadonlyArray<Attribute<Message>>—Class name and additional attributes spread onto the trigger button.
ariaLabelstring—Accessible name for the trigger button. Use for an icon-only trigger with no visible label. Applied as aria-label, and takes precedence over ariaLabelledBy.
ariaLabelledBystring—Id of an external element that labels the trigger button, applied as aria-labelledby. Pair with a visible label element.
previousMonthLabel / nextMonthLabelstring / stringEnglishAccessible labels for the Calendar's previous- and next-month buttons.
previousYearsPageLabel / nextYearsPageLabelstring / stringEnglishAccessible labels for the Calendar's previous- and next-page buttons in Years mode.
daysHeadingButtonLabel / monthsHeadingButtonLabelstring / stringEnglishAccessible labels for switching from Days to Months and from Months to Years.
toDaysGridLabel(monthYear: string) => stringEnglishBuilds the Days grid label from the locale-formatted heading.
toWeekLabel(weekStart: CalendarDate) => stringEnglishBuilds each week-row label from its first date.
toMonthsGridLabel(year: number) => stringEnglishBuilds the Months grid label from its displayed year.
toYearsGridLabel(startYear: number, endYear: number) => stringEnglishBuilds the Years grid label from the bounds of its displayed window.
panelClassName / panelAttributesstring / ReadonlyArray<Attribute<Message>>—Class name and additional attributes spread onto the popover panel.
backdropClassName / backdropAttributesstring / ReadonlyArray<Attribute<Message>>—Class name and additional attributes spread onto the click-outside backdrop.

CalendarAttributes

The discriminated union passed to toCalendarView. Pattern-match on _tag ('Days' | 'Months' | 'Years') with Match.tagsExhaustive to render each grid. Each variant exposes a different shape: Days carries weeks plus a headingButton; Months carries 12 month cells plus a headingButton; Years carries 12 year cells plus prev/next page buttons. See the Calendar page's CalendarAttributes section for the full prop table. The type is the same.

OutMessage

Messages emitted to the parent through the optional outMessage field. Fold the OutMessage in the foldOutMessage of your Update.foldChild config.

NameTypeDefaultDescription
SelectedDate{ date: CalendarDate }—Emitted when the user commits a date (click / Enter / Space). Fold it in the foldOutMessage of your DatePicker fold to lift the date into domain state.
ClearedDate{}—Emitted when the user clears the selected date (via Cleared or DatePicker.clear). The popover stays open. Fold it into the parent-owned selected-date field by setting it to Option.none().
ChangedViewMonth{ year: number; month: number }—Emitted when navigation changes the visible month inside the calendar grid.

Programmatic Helpers

These child entry points let an update commit a controlled selection, open or close the DatePicker after a domain event, or reflect constraints derived from other Model state.

The four reflect* helpers are how you implement cross-field date validation. Constraints are set at init time and updated via these helpers. They do not live in ViewInputs, because the update function needs them for keyboard-navigation disabled-skipping and commit-time validation. For an end date that must be on or after a start date, call reflectMinDate(endDatePicker, maybeStartDate) in the handler that processes the start date change, where endDatePicker is the end date picker's own DatePicker.Model and maybeStartDate is the parent-owned start-date field.

NameTypeDefaultDescription
selectDate(model: Model, date: CalendarDate) => Update.ReturnWithOutMessage<Model, Message, OutMessage>—Commits the given date and closes the popover, emitting SelectedDate. Use for a programmatic selection equivalent to a user pick. To move the embedded calendar onto a date without selecting it (opening onto an externally-sourced value), use focusDate.
focusDate(model: Model, date: CalendarDate) => Model—Moves the embedded calendar view and cursor to a date without changing the selection, which the parent owns. Use it after the parent sets its value from an external source such as a URL or saved draft, so opening the picker shows that month. Returns the Model directly with no Command or OutMessage.
clear(model: Model) => Update.ReturnWithOutMessage<Model, Message, OutMessage>—Clears the selected date, emitting ClearedDate so the parent resets its own field. Does not close the popover.
open(model: Model) => Update.ReturnWithOutMessage<Model, Message, OutMessage>—Programmatically opens the popover and can emit Opened. Use from domain-event handlers when the date picker should open in response to something other than a trigger click.
close(model: Model) => Update.ReturnWithOutMessage<Model, Message, OutMessage>—Programmatically closes the popover and can emit Closed.
reflectMinDate(model: Model, maybeMinDate: Option<CalendarDate>) => Model—Updates the minimum selectable date. Pass Option.none() to remove the minimum. Use it for cross-field validation, for example when an end date picker's minimum tracks a start date picker's selection. It does not reconcile the current selection if it falls below the new minimum.
reflectMaxDate(model: Model, maybeMaxDate: Option<CalendarDate>) => Model—Updates the maximum selectable date. Pass Option.none() to remove the maximum. Does not reconcile the current selection.
reflectDisabledDates(model: Model, disabledDates: ReadonlyArray<CalendarDate>) => Model—Replaces the list of individually disabled dates, such as holidays. Pass an empty array to clear it.
reflectDisabledDaysOfWeek(model: Model, disabledDaysOfWeek: ReadonlyArray<DayOfWeek>) => Model—Replaces the list of disabled days of the week. For example: ["Saturday", "Sunday"]. Pass an empty array to clear it.