Date Picker
A date picker component combining segmented input field with a calendar dropdown. Supports keyboard navigation, single date and range selection, min/max constraints, and full accessibility.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
Overview
The Date Picker component combines two patterns into an interactive widget:
- Input — displays the selected date and accepts typed input
- Calendar — a dropdown grid for visual date selection
Together they form a component that can be opened, navigated, and dismissed — with full keyboard and screen reader support.
Composition
.form-field.date-picker-field
├── label.field-label [for="date-day"]
└── .input-field.date [role="group", aria-labelledby]
├── .date-segments
│ ├── input.date-segment.day [maxlength=2, inputmode=numeric, aria-label="Day"]
│ ├── span.date-separator "/"
│ ├── input.date-segment.month [maxlength=2, inputmode=numeric, aria-label="Month"]
│ ├── span.date-separator "/"
│ └── input.date-segment.year [maxlength=4, aria-label="Year"]
├── .input-field-control
│ └── button [aria-label="Open calendar", aria-expanded, aria-controls]
│ └── span.icon (calendar icon)
└── .calendar [hidden until opened]
├── .calendar-header
└── table.calendar-table [role="grid"]
Patterns Used
| Pattern | Role |
|---|---|
| Input | Text field showing the formatted date |
| Input control button | Calendar toggle trigger, matching clear and increment controls |
| Button ghost | Calendar month navigation |
| Icon | Calendar and chevron icons |
| Calendar | Date grid with selection and keyboard nav |
Interaction
| Action | Result |
|---|---|
| Type 2 digits in Day | Auto-advances focus to Month |
| Type 2 digits in Month | Auto-advances focus to Year |
Press / or . in any segment |
Advances to next segment |
| Backspace on empty segment | Moves focus to previous segment |
| Arrow Left at start of segment | Moves to previous segment |
| Arrow Right at end of segment | Moves to next segment |
| Click label | Focuses Day and opens the calendar |
| Click field surface | Focuses the nearest segment and opens the calendar |
| Click calendar icon | Open/close the calendar dropdown |
| Click a day cell | Fill segments, close calendar |
| Alt + Arrow Down in a segment | Opens the calendar and moves focus to the grid |
| Escape (while calendar open) | Close calendar without selecting |
| Tab | Moves between segments, then to calendar trigger |
States
| State | Description |
|---|---|
| Closed | Only the input field is visible |
| Open | Calendar is visible below the input |
| Date selected | Input shows formatted date, calendar highlights the day |
| Invalid | Input shows error state (out of range, invalid format) |
| Disabled | Both input and calendar trigger are disabled |
Accessibility
- The visible label is associated with the Day segment and names the grouped field
- Calendar trigger button has
aria-label="Open calendar" - Calendar state is exposed with
aria-expandedandaria-controlson the trigger - Pointer-open from the label or field keeps focus in the editable segments
- Button/keyboard-open can move focus into the calendar grid
- All day cells have
aria-labelwith full date - Escape closes without selecting
- Works entirely via keyboard (no mouse required)
Tokens
Reuses tokens from the composed patterns:
- Input tokens (
--input-*) - Input control button behavior from
.input-field-control - Calendar tokens (
--calendar-cell-*)
No additional component-specific tokens needed — the composition inherits from its parts.
Why Custom?
The native <input type="date"> has real accessibility and UX limitations:
| Issue | Native | This Component |
|---|---|---|
| Touch targets | Under 44px (browser controls) | 44×44px minimum per cell |
| Keyboard navigation | Inconsistent across browsers | Full arrow/Home/End/PageUp/PageDown |
| Styling | Shadow DOM, not brand/mode adaptable | Token-based, Brand/Mode aware |
| Range selection | Not supported | Prepared (.is-range-start, .is-range-middle, .is-range-end) |
| Cross-browser consistency | Safari ≠ Chrome ≠ Firefox | Identical behavior everywhere |
| Screen reader experience | Varies, often poor | Explicit aria-label per cell, aria-live for month changes |
| Segment navigation | Browser-dependent | Tab between DD/MM/YYYY, ArrowUp/Down to increment |
This component is a progressive enhancement: it provides a superior experience while maintaining the same semantics and data format as the native date input.
JavaScript
The Date Picker requires src/ui/components/date-input.js for:
- Opening/closing the calendar popover
- Synchronizing input value ↔ calendar selection
- Keyboard event handling (Escape to close, Enter to select)
- Date parsing and formatting
<script type="module">
import { DateInput } from './components/date-input.js';
document.querySelectorAll('.date-input').forEach(el => new DateInput(el));
</script>