Signal FormsCDK Overlay

Date Picker

Unified date / time / date-time picker. <input> + popover for every mode — overlay content swaps based on [mode]. Parses on every keystroke (silently — only emits when valid), reformats canonical on blur. Format driven by WrDateAdapter. A signal-forms native control — it implements FormValueControl, so [formField] binds straight to its value model. [(value)] works standalone, and [(ngModel)] / reactive forms keep working through Angular 22's forms bridge.

Installation

import { WrDatePicker } from 'ngwr/date-picker';
import { provideWrDateAdapter } from 'ngwr/date';

bootstrapApplication(AppComponent, {
  providers: [provideWrDateAdapter()],
});

@Component({ imports: [WrDatePicker] })
export class MyComponent {
  protected readonly picked = signal<Date | null>(null);
}

Basic

Default shortDate format follows the active locale.

<wr-date-picker [(value)]="picked" placeholder="Pick a date" />

Custom format

Pass any token string supported by the adapter: yyyy / yy / MMMM / MMM / MM / M / dd / d / HH / H / hh / h / mm / ss / a.

<wr-date-picker [(value)]="picked" format="dd.MM.yyyy" />

Bounds + filter

min, max, and dateFilter are forwarded to the inner calendar. A typed value the calendar itself would refuse — out of bounds, or filtered out — is not emitted either, so the keyboard and the grid cannot disagree about what is selectable. Both bounds are inclusive, so max names the last selectable day rather than the first excluded one. And grey means only that a cell sits outside the displayed month, never that it is unselectable: an out-of-month cell that is in range and passes the filter can be picked, and the calendar then reopens on the month it landed in. Whether this demo has such a cell depends on the day — isWeekday also applies to it, so in a month whose boundary lands on a weekend every trailing cell is refused.

<wr-date-picker
  [(value)]="picked"
  [min]="today"
  [max]="firstOfNextMonth"
  [dateFilter]="isWeekday"
/>

Time mode

mode="time" replaces the calendar with an HH:MM[:SS] stepper. AM/PM column appears in 12-hour mode. Value is Date | null — the date portion is preserved across edits.

<!-- Time-only: HH:MM stepper with optional AM/PM -->
<wr-date-picker mode="time" [(value)]="picked" />

Time — 24-hour

timeFormat='24h' forces the locale-independent 24h layout.

<wr-date-picker mode="time" timeFormat="24h" [(value)]="picked" />

Time — seconds + step

step applies to both minutes and seconds.

<wr-date-picker
  mode="time"
  timeFormat="24h"
  [showSeconds]="true"
  [step]="5"
  [(value)]="picked"
/>

Date + Time mode

mode="datetime" stacks the calendar above the time stepper. Picking a date does NOT close the overlay — the user usually wants to set the time next.

<!-- Date + time: calendar above, stepper below. Picking a date keeps the
     overlay open so the user can set the time next. -->
<wr-date-picker mode="datetime" [(value)]="when" />

Date + Time — custom format, seconds, step

<wr-date-picker
  mode="datetime"
  format="dd.MM.yyyy HH:mm:ss"
  timeFormat="24h"
  [showSeconds]="true"
  [step]="5"
  [(value)]="when"
/>

Date range

<wr-date-range-picker> is its own component rather than another [mode], because a range value is [start, end] — folding that into the single picker's Date | null would break [formField] inference everywhere it is already used. Everything else matches: same input skeleton, same overlay, same adapter formats. Two inputs share one range calendar; each end is independently typeable, and picking the second date closes the overlay.

import { type WrDateRange, WrDateRangePicker } from 'ngwr/date-picker';

@Component({ imports: [WrDateRangePicker] })
export class MyComponent {
  protected readonly period = signal<WrDateRange | null>(null);
}
<wr-date-range-picker
  [(value)]="period"
  startPlaceholder="From"
  endPlaceholder="To"
/>
Value: — → —

Date range + time

mode="datetime" adds one time stepper per end below the calendar. Picking dates does NOT close the overlay, and moving a date keeps the hours already set on that end.

<!-- One time stepper per end; picking dates keeps the overlay open. -->
<wr-date-range-picker
  mode="datetime"
  timeFormat="24h"
  format="dd.MM.yyyy HH:mm"
  [(value)]="window"
/>
Value: — → —

Keyboard and focus

Which key opens the popup decides whether focus follows it in — a click that placed a caret in the text field is never overruled.

  • The trigger button — opens and moves focus to the calendar's roving cell (the hours field in `time` mode).
  • Alt + ArrowDown in the field — the keyboard equivalent, same destination.
  • ArrowDown / ArrowUp in the field while the popup is already open — walks focus in. This is the way in after opening by clicking the field, which deliberately leaves the caret where it was.
  • Escape, picking a day, or clicking away — closes, and hands focus back to whatever opened it, provided focus was still inside the popup.

Every other key belongs to the text field: typing a date keeps working with the popup open. The popup is not modal and does not trap focus — Tab leaves it, and leaving closes nothing.

wr-date-range-picker follows the same contract across both of its fields, and focus returns to the one that opened the popup — closing from the end field does not throw the caret back to the start. readonly is the one deliberate difference: the range picker refuses to open at all, because two fields feeding one calendar leaves no reading of “untypeable” that still lets the grid rewrite both ends.

Use it in a form

Both pickers are signal-forms native controls, so [formField] binds their value model directly. Reactive forms and [(ngModel)] go through Angular 22's forms bridge, which binds a signal-forms control with no ControlValueAccessor involved — the library ships none, and formControlName still works. Read the guide before relying on updateOn or on a { emitEvent: false } write; neither behaves the way it does for a native <input>. Submit empty to see the required error.

<!-- Signal forms — the native path. -->
<wr-date-picker [formField]="form.due" />

<!-- Reactive forms. No ControlValueAccessor exists in the library and none is
     needed: Angular 22 binds the control's `value` model, relays its `touch`
     output as markAsTouched(), and pushes `disabled` down from the control.
     The control's value is a `Date | null` — the picker never emits a
     half-typed date, so an unparseable field leaves the last valid one. -->
<form [formGroup]="form">
  <wr-form-field label="Due date" required>
    <wr-date-picker formControlName="due" placeholder="Pick a date" />
    <wr-form-error key="required">Pick a due date.</wr-form-error>
  </wr-form-field>
</form>

<!-- The range picker is the same control with a `WrDateRange | null` value. -->
<wr-date-range-picker formControlName="period" />

<!-- Template-driven — the same bridge. -->
<wr-date-picker [(ngModel)]="due" name="due" />
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';

import { type WrDateRange, WrDatePicker, WrDateRangePicker } from 'ngwr/date-picker';
import { WrFormError, WrFormField } from 'ngwr/form';

@Component({
  imports: [ReactiveFormsModule, WrFormField, WrFormError, WrDatePicker, WrDateRangePicker],
  templateUrl: './my.html',
})
export class MyComponent {
  protected readonly form = new FormGroup({
    due: new FormControl<Date | null>(null, Validators.required),
    period: new FormControl<WrDateRange | null>(null),
  });
}
due touched: false · form valid: false

API

NameDescriptionTypeDefault
modePicker behavior — see class doc.'date' | 'time' | 'datetime''date'
formatFormat used for both display and parsing. When null (default), the format is derived from mode (shortDate / time / shortDateTime). Pass a named key or raw token string to override.WrDateFormat | string | nullnull
placeholderPlaceholder shown when the input is empty.string''
minMin selectable date (forwarded to the calendar). Ignored in time mode.Date | undefinedundefined
maxMax selectable date (forwarded to the calendar). Ignored in time mode.Date | undefinedundefined
dateFilterPredicate to disable specific dates (forwarded to the calendar). Ignored in time mode.((date: Date) => boolean) | nullnull
timeFormatTime-panel 12 / 24-hour format. Applies in time + datetime modes.'auto' | '12h' | '24h''auto'
showSecondsRender the seconds column. Applies in time + datetime modes.booleanfalse
stepMinute / second step for the time panel.number1
disabledDisable interaction. Bound automatically from the field's disabled state when used with [formField].booleanfalse
readonlyRead-only — input not typeable, but the trigger icon still opens the overlay.booleanfalse
valueThe picked Date. Bound by [formField], or two-way via [(value)].Date | nullnull
(touch)Emitted on blur so a bound field can mark itself touched.void
ariaLabelAccessible name of the text field. Falls back to the placeholder, then to the same catalog string the calendar button uses — the field is a role="combobox", and with an empty placeholder it had no name at all.string | nullnull
panelAriaLabelAccessible name of the popup. The trigger advertises aria-haspopup="dialog", so the panel is a role="dialog" — and an unnamed dialog announces as a bare "dialog". Defaults to the catalog's datePicker.panel* string for the current mode.string | nullnull

Range picker API

NameDescriptionTypeDefault
valueThe picked range as [start, end]. Either end may be null while half-picked; out-of-order ends are swapped on commit.WrDateRange | nullnull
modedate (default) renders a range calendar; datetime adds a time stepper per end.'date' | 'datetime''date'
formatDisplay + parse format for both ends. When omitted, derived from mode (shortDate / shortDateTime).WrDateFormat | string | nullnull
startPlaceholderPlaceholder for the start input.string''
endPlaceholderPlaceholder for the end input.string''
separatorGlyph rendered between the two inputs.string'–'
minDateEarliest selectable date, both ends. Named minDate because signal forms reserve min for the value type — here a range.Date | nullnull
maxDateLatest selectable date, both ends.Date | nullnull
dateFilterPredicate disabling individual dates.((date: Date) => boolean) | nullnull
timeFormatTime-panel 12 / 24-hour format. Applies in datetime mode.'auto' | '12h' | '24h''auto'
showSecondsRender the seconds column on both time panels.booleanfalse
stepMinute / second step for the time panels.number1
disabledBlock interaction.booleanfalse
readonlyInputs are not typeable; trigger icon still opens the overlay.booleanfalse

CSS variables

Custom properties ngwr/date-picker publishes. Each default below is declared on the component's own selector, so a :root override is shadowed by it — set them on that selector, on a wrapper you scope yourself, or inline on the element. Unlike the BEM class names, these are the supported way to restyle the component.

VariableDefaultDeclared on
--wr-time-picker-bgvar(--wr-color-surface).wr-time-picker
--wr-time-picker-bordervar(--wr-color-outline).wr-time-picker
--wr-time-picker-col-width3rem.wr-time-picker
--wr-time-picker-colorvar(--wr-color-on-surface).wr-time-picker
--wr-time-picker-mutedvar(--wr-color-on-surface-muted).wr-time-picker