Date picker (single, range, time)
components specs/components/date-picker.kmd
Accessible date picker — typed ISO entry + calendar grid for single date / date range / date+time. Replaces native `<input type="date">` (inconsistent across browsers) and ad-hoc third-party widgets. Modeled after Duet (LocalTapiola), Polaris, Spectrum.
When this spec applies
Primary triggers
- Pick a date input component for a Koder product
All triggers
- Add a date input to a form
- Surface a date range filter
Specification body
Component — Date picker
Status: v0.1.0 — Draft. Three modes: single / range / time.
R1 — Input cell
- Text input accepting typed ISO
YYYY-MM-DD(or locale-formatted, see R7). - Trailing calendar icon opens the popover.
- Inline parse errors render below the cell ("Invalid date"); never silently auto-correct.
R2 — Calendar grid (popover)
- 6×7 grid; days outside current month rendered with muted style.
- Month/year header with previous/next arrows + click-month-name to open year/month quick-pick.
- Today's date marked with outline ring (not filled — selection takes precedence).
- Disabled dates rendered muted + not focusable; live region announces blocked attempt.
R3 — Range mode
- Two paired inputs (start + end) sharing a single calendar popover.
- Selecting start highlights tentative range while user picks end.
- Preset shortcuts row above grid:
Today,Yesterday,Last 7 days,Last 30 days,This month(configurable per product).
R4 — Time mode
- Single mode: input includes time component
YYYY-MM-DD HH:mm. - Popover adds hour/minute scrolls (or 24-hour input + AM/PM selector per R7 locale).
- Seconds default off; togglable per instance.
R5 — Keyboard navigation
| Key | Action |
|---|---|
| ↑/↓/←/→ | Day-by-day within grid |
| PgUp / PgDn | Month-by-month |
| Shift+PgUp / Shift+PgDn | Year-by-year |
| Home / End | Start / end of week |
| Enter | Select highlighted date |
| Esc | Close popover; restore prior value |
R6 — Screen-reader
- Input:
<input>with associated label +aria-describedbypointing at format hint. - Calendar popover:
role="dialog"witharia-labelledby(month/year heading). - Grid:
role="grid"; daysrole="gridcell"; selectedaria-selected="true". - Live-region: "Selected: {locale-formatted date}".
R7 — i18n
- First-day-of-week per locale (Sunday for en-US; Monday for pt-BR; configurable).
- Month / day names from locale data.
- Input format per locale: en-US
MM/DD/YYYY; pt-BRDD/MM/YYYY; ISO always accepted alongside. - 12h / 24h time per locale.
R8 — Disabled / min / max
minandmaxprops clamp the calendar; dates outside are disabled.- Per-date disable callback for arbitrary blackouts (e.g., weekends).
R9 — OUIA
Per specs/testing/ouia-test-hooks.kmd:
data-ouia-component-type="DatePicker"data-ouia-safe="true"only when popover idle.
Não-escopo
- Time-zone picker (separate spec if pursued).
- Multi-date select (cherry-pick non-range) — separate variant if demand surfaces.
- Recurring-date rules (RFC 5545) — out of v0.
Requirements (testable)
Requirement: Input cell parses ISO and reports errors {#req-date-picker-input-cell}
The input cell SHALL accept a typed ISO YYYY-MM-DD value and, on unparseable input, SHALL render an inline error below the cell without silently auto-correcting the entry. A trailing calendar icon SHALL open the popover.
Scenario: Valid ISO date is accepted
- GIVEN a date picker in single mode with an empty input cell
- WHEN the user types
2026-07-20into the input - THEN the cell's committed value equals
2026-07-20 - AND no inline error is rendered below the cell
Scenario: Invalid date shows inline error and is not corrected
- GIVEN a date picker in single mode
- WHEN the user types
2026-13-45into the input - THEN an inline error reading "Invalid date" is rendered below the cell
- AND the input value is left as typed, not auto-corrected to a valid date
Scenario: Calendar icon opens the popover
- GIVEN a date picker with the popover closed
- WHEN the user activates the trailing calendar icon
- THEN the calendar popover becomes visible
Requirement: Calendar grid marks today and disables out-of-range dates {#req-date-picker-calendar-grid}
The calendar popover SHALL render a 6×7 grid with today's date marked by an outline ring rather than a fill, and dates outside the min/max bounds SHALL be rendered muted and non-focusable, with a live-region announcement when a blocked date is attempted.
Scenario: Today is marked with an outline ring
- GIVEN a calendar popover open on the current month with no date selected
- WHEN the grid renders
- THEN today's gridcell has an outline ring style
- AND today's gridcell is not rendered with the filled selection style
Scenario: Dates outside min/max are disabled
- GIVEN a date picker with
min="2026-07-10"andmax="2026-07-20" - WHEN the calendar grid renders for July 2026
- THEN the gridcell for
2026-07-05is rendered muted - AND the gridcell for
2026-07-05is not focusable
Requirement: Range mode uses paired inputs and a tentative highlight {#req-date-picker-range-mode}
In range mode the picker SHALL present two paired inputs (start and end) that share a single calendar popover, and after the start date is chosen it SHALL highlight the tentative range as the user moves toward selecting the end date.
Scenario: Selecting start shows a tentative range
- GIVEN a range-mode picker with start and end empty and the shared popover open
- WHEN the user selects
2026-07-10as the start date and hovers2026-07-14 - THEN the gridcells from
2026-07-10through2026-07-14are highlighted as a tentative range - AND the end input has not yet committed a value
Scenario: Both inputs share one popover
- GIVEN a range-mode picker
- WHEN the user opens the popover from the start input and then from the end input
- THEN the same single calendar popover instance is used for both
Requirement: Keyboard navigation drives the grid {#req-date-picker-keyboard}
The calendar grid SHALL support keyboard navigation: arrow keys move day-by-day, PageUp/PageDown move month-by-month, Enter selects the highlighted date, and Escape SHALL close the popover and restore the prior value.
Scenario: Arrow key moves highlight by one day
- GIVEN an open calendar with
2026-07-20highlighted - WHEN the user presses the Right arrow key
- THEN the highlighted date becomes
2026-07-21
Scenario: Enter selects the highlighted date
- GIVEN an open calendar with
2026-07-21highlighted - WHEN the user presses Enter
- THEN the input's committed value becomes
2026-07-21
Scenario: Escape restores the prior value
- GIVEN an input committed to
2026-07-20with the popover open and2026-07-28highlighted - WHEN the user presses Escape
- THEN the popover closes
- AND the input value remains
2026-07-20
Requirement: Screen-reader semantics on input and grid {#req-date-picker-screen-reader}
The picker SHALL expose accessible semantics: the input SHALL have an associated label and aria-describedby pointing at the format hint, the popover SHALL be role="dialog" labelled by the month/year heading, and grid days SHALL be role="gridcell" with the selected day carrying aria-selected="true".
Scenario: Popover exposes dialog role and label
- GIVEN a date picker
- WHEN the popover opens
- THEN the popover element has
role="dialog" - AND its
aria-labelledbyreferences the month/year heading element
Scenario: Selected day is marked aria-selected
- GIVEN an open calendar with
2026-07-20selected - WHEN the grid renders
- THEN the gridcell for
2026-07-20hasrole="gridcell" - AND that gridcell has
aria-selected="true"
Requirement: OUIA test hooks {#req-date-picker-ouia}
Per specs/testing/ouia-test-hooks.kmd, the picker SHALL expose data-ouia-component-type="DatePicker" and SHALL set data-ouia-safe="true" only while the popover is idle.
Scenario: OUIA safe flag reflects popover activity
- GIVEN a date picker root element with
data-ouia-component-type="DatePicker" - WHEN the popover is idle (closed and not animating)
- THEN the root element has
data-ouia-safe="true" - WHEN the popover is opening or animating
- THEN the root element has
data-ouia-safe="false"
References
specs/components/buttons.kmdspecs/components/combobox.kmdspecs/i18n/contract.kmdspecs/testing/ouia-test-hooks.kmd