DatePicker

July 6, 2026 · View on GitHub

Input-triggered calendar popup. Single date or date-range, with optional preset panel.

Contract

<kp-date-picker> renders an Input-like trigger. Clicking it opens a portaled calendar panel where the user navigates months / years and picks either a single day (mode="single") or a two-click range (mode="range"). Value is exposed via ControlValueAccessor, so ngModel / reactive forms work without any glue.

Anatomy

$ \text{DatePicker} ├─ \text{Trigger} — \text{Input}-\text{shaped} \text{button} \text{with} \text{calendar} \text{icon} │ ├─ \text{Value} / \text{placeholder} │ ├─ \text{Clear} ( \times ) — \text{appears} \text{when} \text{a} \text{value} \text{is} \text{set} │ └─ \text{Calendar} \text{icon} └─ \text{Panel} (\text{portaled} \text{to} <\text{body}>) ├─ \text{Presets} \text{column} (\text{optional}) └─ \text{Calendar} ├─ \text{Header} — \text{prev} / \text{month}-\text{year} \text{title} / \text{next} ├─ \text{Weekday} \text{row} — \text{Mo} \text{Tu} \text{We} \text{Th} \text{Fr} \text{Sa} \text{Su} (\text{or} \text{Sun}-\text{first}) └─ \text{Grid} — 42 \text{day} \text{cells} / 12 \text{months} / 12 \text{years} $

Sizes

Shares the Input size ramp (xs / sm / md / lg / xl) — the trigger height, radius, typography, and clear-button scale mirror <kp-input> so the two sit flush in a form row.

API

Inputs

NameTypeDefaultDescription
size'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Size ramp
mode'single' | 'range''single'Single date or range
placeholderstring'Select date'Trigger text when empty
minDate | nullnullEarliest selectable date
maxDate | nullnullLatest selectable date
showClearbooleantrueShow the × clear affordance
showPresetsbooleanfalseShow the preset column on the left of the panel
presetsKpDatePickerPreset[] | nullnullOverride preset list (defaults cover Today / Yesterday / Last 7 · 30d / This · Last month)
firstDayOfWeek0 | 11Sunday (0) or Monday (1) start
dateFormatter((d: Date) => string) | nullnullCustom formatter for the trigger text
rangeSeparatorstring' – 'Separator between start and end in the trigger
disabledbooleanfalseDisable interaction
forceStateKpState | nullnullPin visual state for docs

Outputs

NamePayloadFires when
valueChangeDate | [Date | null, Date | null] | nullUser commits a new date / range, or clears
openChangebooleanPanel opens / closes

Forms

Implements ControlValueAccessor. In single mode ngModel is Date \| null; in range mode it's [Date \| null, Date \| null].

<!-- Single -->
<kp-date-picker [(ngModel)]="birthday"/>

<!-- Range -->
<kp-date-picker mode="range" [(ngModel)]="reportRange"/>

<!-- Range with presets -->
<kp-date-picker mode="range" [showPresets]="true" [(ngModel)]="dates"/>

Presets

interface KpDatePickerPreset {
  label: string;
  // Returns a single Date for single mode or a [start, end] tuple for range mode.
  value: () => Date | [Date, Date];
}

Built-in defaults (used when presets is left null):

LabelValue
Todaytoday
Yesterdayyesterday
Last 7 days[today − 6, today]
Last 30 days[today − 29, today]
This month[first of this month, today]
Last month[first of last month, last of last month]

States

StateBehavior
restInput-styled trigger, no panel
hoverTrigger border darkens
focus / openTrigger border turns blue; panel visible (portaled)
disabledMuted trigger, no interaction
errorRed border

Accessibility

  • Trigger is a <button> with aria-haspopup="dialog" and aria-expanded.
  • Panel carries role="dialog" with aria-label="Choose date".
  • Day cells are real <button>s, so Tab / Shift+Tab move between them.
  • Esc closes the panel.
  • Click-outside (on both the host and the portaled panel) closes cleanly.
  • Disabled days (outside min/max) use native disabled so they can't receive focus.

Do / Don't

Do

  • Use showPresets for reports, analytics and any range picker where common windows dominate usage — it cuts picks from several clicks to one.
  • Constrain with min / max for fields like "date of birth" (no future) or "appointment" (no past).
  • Supply a dateFormatter to align with the host app's locale / format.

Don't

  • Don't use range mode for a single-value field — use two separate single pickers if start and end are independent and can be cleared individually.
  • Don't rely on the built-in format for locales other than English — pass dateFormatter with your app's Intl.DateTimeFormat instance.

References

Changelog

  • 0.1.0 — Initial release. Single + range, 5 sizes, presets panel, min/max constraints, day/month/year views, portal-to-body panel, full CVA support.