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
| Name | Type | Default | Description |
|---|---|---|---|
size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Size ramp |
mode | 'single' | 'range' | 'single' | Single date or range |
placeholder | string | 'Select date' | Trigger text when empty |
min | Date | null | null | Earliest selectable date |
max | Date | null | null | Latest selectable date |
showClear | boolean | true | Show the × clear affordance |
showPresets | boolean | false | Show the preset column on the left of the panel |
presets | KpDatePickerPreset[] | null | null | Override preset list (defaults cover Today / Yesterday / Last 7 · 30d / This · Last month) |
firstDayOfWeek | 0 | 1 | 1 | Sunday (0) or Monday (1) start |
dateFormatter | ((d: Date) => string) | null | null | Custom formatter for the trigger text |
rangeSeparator | string | ' – ' | Separator between start and end in the trigger |
disabled | boolean | false | Disable interaction |
forceState | KpState | null | null | Pin visual state for docs |
Outputs
| Name | Payload | Fires when |
|---|---|---|
valueChange | Date | [Date | null, Date | null] | null | User commits a new date / range, or clears |
openChange | boolean | Panel 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):
| Label | Value |
|---|---|
| Today | today |
| Yesterday | yesterday |
| 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
| State | Behavior |
|---|---|
| rest | Input-styled trigger, no panel |
| hover | Trigger border darkens |
| focus / open | Trigger border turns blue; panel visible (portaled) |
| disabled | Muted trigger, no interaction |
| error | Red border |
Accessibility
- Trigger is a
<button>witharia-haspopup="dialog"andaria-expanded. - Panel carries
role="dialog"witharia-label="Choose date". - Day cells are real
<button>s, soTab/Shift+Tabmove between them. Esccloses the panel.- Click-outside (on both the host and the portaled panel) closes cleanly.
- Disabled days (outside
min/max) use nativedisabledso they can't receive focus.
Do / Don't
Do
- Use
showPresetsfor reports, analytics and any range picker where common windows dominate usage — it cuts picks from several clicks to one. - Constrain with
min/maxfor fields like "date of birth" (no future) or "appointment" (no past). - Supply a
dateFormatterto 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
dateFormatterwith your app'sIntl.DateTimeFormatinstance.
References
- Figma component:
DatePickerComponent Set - Storybook: https://gregnblack.github.io/kanso-protocol/?path=/docs/components-datepicker
- Source:
packages/ui/datepicker/src/ - Tokens used:
- Panel:
datepicker/panel-bg,datepicker/panel-border - Header:
datepicker/header-fg,datepicker/header-nav-fg - Weekday row:
datepicker/weekday - Day cell:
datepicker/day-bg-*,datepicker/day-fg-*,datepicker/day-today-ring
- Panel:
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.