Skip to content

FRD-026: Date/Time Family Expansion

FieldValue
OwnerDavid Holmes
StatusDraft
Last Updated2026-05-26
Target Releasev2.0.0
TypeWidget
SizeL
PriorityP1 — Component Improvement

Document Summary

Expand the date/time component family with TimePicker, DateTimePicker, MonthPicker, and RelativeDateSelect. These components complete the temporal input surface by building on the existing DatePicker, DateRangePicker, and InlineCalendar primitives. All new components must be fully keyboard accessible, support time zone handling, honor reduced-motion preferences, and ship with Storybook stories including controls.


Introduction

Overview

The design system provides DatePicker, DateRangePicker, and InlineCalendar for date selection but lacks dedicated components for time-only input, combined date-time input, month-level granularity, and human-friendly relative date selection (e.g. “Last 7 days”). SaaS dashboards, scheduling UIs, and reporting filters all require these patterns.

Goals

  • Provide a TimePicker component for standalone time selection with configurable step intervals.
  • Provide a DateTimePicker that composes DatePicker and TimePicker into a single field.
  • Provide a MonthPicker for year-month selection without day granularity.
  • Provide a RelativeDateSelect for preset ranges (“Today”, “Last 30 days”, “This quarter”) with optional custom-range escape hatch.
  • Maintain API consistency with the existing DatePicker props contract (value as ISO string, onChange callback, label, error, hint, disabled, required).
  • Deliver complete time zone support via an optional timeZone prop using IANA identifiers.

Non-Goals

  • Building a full scheduling/calendar widget (event placement, drag-to-resize).
  • Server-side date parsing or validation libraries.
  • Replacing the existing DatePicker or DateRangePicker APIs; those remain stable.

Scope

In Scope

ItemDescription
TimePickerDropdown or scroll-wheel time selector with hour/minute/optional-second inputs and AM/PM toggle.
DateTimePickerCombined date + time field composing DatePicker and TimePicker.
MonthPickerYear-month selector with calendar-grid or dropdown-based navigation.
RelativeDateSelectPreset-based select with configurable presets and optional custom DateRangePicker fallback.
Time zone supportOptional timeZone prop (IANA string) on TimePicker and DateTimePicker; display offset label.
Keyboard navigationArrow keys, Enter, Escape, Tab for all new components per WAI-ARIA date/time patterns.
Reduced-motionAll open/close animations respect prefers-reduced-motion via existing motion tokens.
Storybook storiesOne story file per component with controls, dark mode, RTL, and disabled/error states.
Unit testsCore interaction flows, keyboard navigation, edge cases (midnight rollover, DST transitions).

Out of Scope

ItemReason
Recurring schedule builderSeparate widget; different complexity tier.
Natural language date parsingRequires NLP dependency; out of scope for a primitive.
Server-side validationConsumer responsibility; we provide Zod schema examples in docs only.
Calendar event renderingBelongs in a calendar application widget, not the date family.

Users and Pain Points

UserPain Point
SaaS developers building scheduling UIsMust hand-roll time pickers or pull in a third-party library that clashes with design tokens.
Dashboard buildersNo native relative-date preset selector; every team reinvents “Last 7 days” dropdowns.
Form designersDateTimePicker requires combining DatePicker with a raw input[type=time], breaking visual consistency.
Reporting/analytics teamsMonthPicker forces a full DatePicker where only year-month granularity is needed, confusing end users.
Users with motor impairmentsTime inputs without proper keyboard navigation are unusable.

Definitions

TermDefinition
IANA time zoneA time zone identifier from the IANA Time Zone Database, e.g. America/New_York.
ISO 8601International date/time string standard. DatePicker uses YYYY-MM-DD; TimePicker will use HH:mm or HH:mm:ss.
Step intervalThe minute increment for TimePicker options (e.g. 15 = :00, :15, :30, :45).
Relative date presetA named time range resolved at render time relative to “now” (e.g. “Last 7 days”).
CalendarDateThe internal @internationalized/date value object used by the existing DatePicker.

Current State

  • DatePicker (src/components/ui/date-picker.tsx): Fully functional single-date picker using React Aria Calendar primitives. Value is YYYY-MM-DD string. Supports label, error, hint, disabled, required, min/max dates.
  • DateRangePicker (src/components/ui/date-range-picker.tsx): Two-calendar range picker returning { start: string; end: string }.
  • InlineCalendar (src/components/ui/inline-calendar.tsx): Always-visible calendar grid for embedding in panels.
  • No TimePicker, DateTimePicker, MonthPicker, or RelativeDateSelect exist.
  • Motion tokens in src/styles/motion.css define --motion-overlay (220ms) and reduced-motion overrides.
  • Form field utilities (useFieldId, joinDescribedBy, getFieldShellClassName) standardize field chrome.

Proposed Solution

Build four new components in src/components/ui/:

  1. TimePicker — A controlled input displaying formatted time with a popover dropdown listing selectable time slots at the configured step interval. Uses React Aria’s time field primitives for accessibility. Emits HH:mm or HH:mm:ss strings.

  2. DateTimePicker — Composes DatePicker (for the date portion) and TimePicker (for the time portion) in a single field group. Value is an ISO 8601 datetime string (YYYY-MM-DDTHH:mm). The popover shows the calendar with a time selector below it.

  3. MonthPicker — A button trigger opening a popover grid of months within a navigable year. Value is YYYY-MM string. Year navigation via chevron buttons.

  4. RelativeDateSelect — A Select-like component whose options are developer-defined presets (each with a label and a resolve function returning { start: Date; end: Date }). An optional allowCustom prop appends a “Custom range…” option that opens a DateRangePicker inline.

All four use the established field shell (getFieldShellClassName), label (FieldLabelContent), and field-id conventions. Time zone display is handled by an optional trailing badge showing the UTC offset.


Requirements

Requirement Priorities

  • Must Have: TimePicker, DateTimePicker, MonthPicker, RelativeDateSelect with keyboard navigation and Storybook stories.
  • Should Have: Time zone display, 12h/24h toggle, RelativeDateSelect custom range escape hatch.
  • Could Have: Scroll-wheel time selection mode, voice-over optimized announcements.
  • Won’t Have (this release): Recurring schedule patterns, NLP parsing.

Functional Requirements

IDRequirementPriority
FR-01TimePicker accepts value (HH:mm string), onChange, step (minutes, default 15), label, error, hint, disabled, required.Must
FR-02TimePicker supports 12-hour (AM/PM) and 24-hour display via hourCycle prop.Should
FR-03TimePicker popover lists selectable time slots computed from step; active slot is highlighted.Must
FR-04DateTimePicker accepts value (ISO datetime string), onChange, and proxies date/time sub-props.Must
FR-05DateTimePicker popover shows calendar above and time selector below in a single overlay.Must
FR-06MonthPicker accepts value (YYYY-MM), onChange, minMonth, maxMonth, label, error.Must
FR-07MonthPicker renders a 4x3 month grid within a year; year navigation via chevrons.Must
FR-08RelativeDateSelect accepts presets array of { id, label, resolve } and emits the resolved range.Must
FR-09RelativeDateSelect allowCustom prop appends a “Custom range” option that opens a DateRangePicker.Should
FR-10All four components support timeZone prop (IANA string) for display and value interpretation.Should
FR-11All four components expose ref forwarding to the outer container element.Must

Non-Functional Requirements

IDRequirementTarget
NFR-01Bundle size per component< 8 KB gzipped (excluding shared React Aria deps).
NFR-02Render performance< 16ms initial render for TimePicker dropdown with 96 slots (15-min step, 24h).
NFR-03Reduced-motion complianceAll transitions collapse to ≤ 80ms when prefers-reduced-motion: reduce is active.
NFR-04Dark modeFull token-based dark mode support with no hardcoded colors.
NFR-05SSR safetyAll components render without window/document access on first pass.

API/Interface Requirements

TimePicker

interface TimePickerProps {
value?: string; // "HH:mm" or "HH:mm:ss"
onChange?: (time: string) => void;
step?: number; // minutes, default 15
hourCycle?: 12 | 24; // default 12
timeZone?: string; // IANA identifier
label?: string;
error?: string;
hint?: string;
disabled?: boolean;
required?: boolean;
className?: string;
}

DateTimePicker

interface DateTimePickerProps {
value?: string; // "YYYY-MM-DDTHH:mm"
onChange?: (datetime: string) => void;
timeStep?: number;
hourCycle?: 12 | 24;
timeZone?: string;
minDate?: string;
maxDate?: string;
label?: string;
error?: string;
hint?: string;
disabled?: boolean;
required?: boolean;
className?: string;
}

MonthPicker

interface MonthPickerProps {
value?: string; // "YYYY-MM"
onChange?: (month: string) => void;
minMonth?: string;
maxMonth?: string;
label?: string;
error?: string;
hint?: string;
disabled?: boolean;
required?: boolean;
className?: string;
}

RelativeDateSelect

interface RelativeDatePreset {
id: string;
label: string;
resolve: () => { start: Date; end: Date };
}
interface RelativeDateSelectProps {
presets: RelativeDatePreset[];
value?: string; // preset id or "custom"
onChange?: (presetId: string, range: { start: Date; end: Date }) => void;
allowCustom?: boolean;
label?: string;
error?: string;
disabled?: boolean;
className?: string;
}

Accessibility Requirements

IDRequirement
A11Y-01TimePicker listbox follows WAI-ARIA Listbox pattern; arrow keys navigate, Enter selects, Escape closes.
A11Y-02DateTimePicker combines DatePicker and TimePicker ARIA patterns; focus moves naturally between date and time sections.
A11Y-03MonthPicker grid uses role="grid" with role="gridcell" per month; arrow keys navigate the 4x3 grid.
A11Y-04RelativeDateSelect uses native Select semantics; custom range sub-panel is announced as an expanded region.
A11Y-05All components announce selected values via aria-live="polite" region.
A11Y-06Focus is trapped within open popovers and restored to the trigger on close.
A11Y-07All components pass axe-core automated checks with zero violations.

Content and Documentation Requirements

IDRequirement
DOC-01Each component gets a Storybook docs page with usage guidelines, do/don’t examples, and prop table.
DOC-02Migration guide for teams currently using raw &lt;input type="time"&gt; alongside DatePicker.
DOC-03Zod validation recipe showing datetime string parsing and timezone-aware validation.
DOC-04Time zone handling guide explaining IANA identifiers, offset display, and DST edge cases.

Dependencies

DependencyTypeRisk
react-aria-componentsExistingLow — already used by DatePicker.
@internationalized/dateExistingLow — already used for CalendarDate.
src/components/ui/date-picker.tsxInternalLow — DateTimePicker composes it.
src/components/ui/date-range-picker.tsxInternalLow — RelativeDateSelect uses it for custom range.
src/styles/motion.cssInternalLow — motion tokens already defined.
src/lib/form-control-styles.tsInternalLow — field shell shared across all form controls.

Risks and Tradeoffs

RiskLikelihoodImpactMitigation
Time zone handling complexity across DST boundariesMediumHighLean on @internationalized/date for zone-aware arithmetic; add DST-specific unit tests.
TimePicker dropdown with 1-minute steps renders 1440 itemsLowMediumVirtualize the listbox if step < 5; default step to 15.
DateTimePicker popover may be too tall on small screensMediumMediumCollapse to stacked layout below 480px viewport width.
API surface grows significantly with four new componentsLowLowShared prop patterns and consistent naming reduce cognitive load.

Open Questions

#QuestionOwnerStatus
OQ-01Should TimePicker support seconds granularity in v2.0.0 or defer?David HolmesOpen
OQ-02Should RelativeDateSelect support fiscal-year-aware presets?David HolmesOpen
OQ-03Should MonthPicker allow multi-month selection for range use cases?David HolmesOpen
OQ-04Should DateTimePicker emit UTC-normalized strings or local time?David HolmesOpen

Acceptance Criteria

#Criterion
AC-01TimePicker renders, accepts keyboard navigation (arrows, Enter, Escape), and emits valid HH:mm strings.
AC-02DateTimePicker combines date and time selection into a single ISO datetime string output.
AC-03MonthPicker renders a 4x3 grid, navigates years, and emits YYYY-MM strings.
AC-04RelativeDateSelect resolves presets to date ranges and optionally opens a custom range picker.
AC-05All components render correctly in dark mode and with prefers-reduced-motion: reduce.
AC-06All components pass axe-core checks with zero violations.
AC-07Storybook stories exist for each component with controls for all props.
AC-08Unit tests cover happy path, keyboard navigation, edge cases (midnight, DST), and disabled/error states.
AC-09pnpm typecheck and pnpm vitest run --project unit pass with zero errors.

LLM Handoff Instructions

When implementing this FRD:

  1. Start with TimePicker — it is the foundational primitive. Build it in src/components/ui/time-picker.tsx following the DatePicker pattern: React Aria primitives, getFieldShellClassName for the trigger, FieldLabelContent for the label, popover with dropdownOverlayPanelClassName.
  2. Then DateTimePicker — compose DatePicker + TimePicker. Put in src/components/ui/date-time-picker.tsx. The popover should render the calendar above and time selector below.
  3. Then MonthPickersrc/components/ui/month-picker.tsx. Use a similar popover pattern but replace the day grid with a 4x3 month grid.
  4. Then RelativeDateSelectsrc/components/ui/relative-date-select.tsx. Wrap the existing Select component; the custom range mode should conditionally render DateRangePicker.
  5. Stories go in sibling .stories.tsx files. Follow the DatePicker stories structure (Default, WithError, Disabled, DarkMode, Controls).
  6. Tests go in sibling .test.tsx files. Test keyboard flows with @testing-library/react and userEvent.
  7. Read ADR-004 (motion) before adding any animation; use existing motion tokens from motion.css.
  8. Read ADR-002 (date/time conventions) for value format decisions.

Key files to reference:

  • src/components/ui/date-picker.tsx — prop pattern, field shell usage, popover structure.
  • src/components/ui/date-range-picker.tsx — range value contract.
  • src/lib/calendar-date.ts — date parsing/formatting utilities.
  • src/lib/form-control-styles.tsgetFieldShellClassName.
  • src/lib/form-field.tsuseFieldId, joinDescribedBy.
  • src/styles/motion.css — motion tokens and reduced-motion overrides.

Decision Log

DateDecisionRationale
2026-05-26Use ISO string values (HH:mm, YYYY-MM, YYYY-MM-DDTHH:mm) for all components.Consistent with DatePicker’s YYYY-MM-DD string contract; avoids Date object serialization issues.
2026-05-26Default TimePicker step to 15 minutes.Covers the vast majority of scheduling UIs; 1-minute precision available via prop.
2026-05-26Compose DateTimePicker from DatePicker + TimePicker rather than building monolithically.Reduces duplication; each sub-component remains independently usable.
2026-05-26RelativeDateSelect uses a resolve function per preset rather than static date offsets.Allows fiscal-year, business-day, and other custom logic without framework changes.

Document History

VersionDateAuthorChanges
0.12026-05-26David HolmesInitial draft.