Skip to content

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

All triggers

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

KeyAction
↑/↓/←/→Day-by-day within grid
PgUp / PgDnMonth-by-month
Shift+PgUp / Shift+PgDnYear-by-year
Home / EndStart / end of week
EnterSelect highlighted date
EscClose popover; restore prior value

R6 — Screen-reader

  • Input: <input> with associated label + aria-describedby pointing at format hint.
  • Calendar popover: role="dialog" with aria-labelledby (month/year heading).
  • Grid: role="grid"; days role="gridcell"; selected aria-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-BR DD/MM/YYYY; ISO always accepted alongside.
  • 12h / 24h time per locale.

R8 — Disabled / min / max

  • min and max props 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-20 into 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-45 into 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" and max="2026-07-20"
  • WHEN the calendar grid renders for July 2026
  • THEN the gridcell for 2026-07-05 is rendered muted
  • AND the gridcell for 2026-07-05 is 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-10 as the start date and hovers 2026-07-14
  • THEN the gridcells from 2026-07-10 through 2026-07-14 are 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-20 highlighted
  • 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-21 highlighted
  • 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-20 with the popover open and 2026-07-28 highlighted
  • 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-labelledby references the month/year heading element

Scenario: Selected day is marked aria-selected

  • GIVEN an open calendar with 2026-07-20 selected
  • WHEN the grid renders
  • THEN the gridcell for 2026-07-20 has role="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