# 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](https://ngwr.dev/guides/forms).

Source: https://ngwr.dev/reference/components/date-picker  
Kind: Signal Forms, CDK Overlay

## Installation

```angular-ts
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.

```angular-html
<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`.

```angular-html
<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.

```angular-html
<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.

```angular-html
<!-- 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.

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

## Time — seconds + step

`step` applies to both minutes and seconds.

```angular-html
<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.

```angular-html
<!-- 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

```angular-html
<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.

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

@Component({ imports: [WrDateRangePicker] })
export class MyComponent {
  protected readonly period = signal<WrDateRange | null>(null);
}
```

```angular-html
<wr-date-range-picker
  [(value)]="period"
  startPlaceholder="From"
  endPlaceholder="To"
/>
```

## 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.

```angular-html
<!-- 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"
/>
```

## 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](https://ngwr.dev/guides/forms), 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.

```angular-html
<!-- 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" />
```

```angular-ts
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),
  });
}
```

## API

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `mode` | Picker behavior — see class doc. | `'date' \| 'time' \| 'datetime'` | `'date'` |
| `format` | Format 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 \| null` | `null` |
| `placeholder` | Placeholder shown when the input is empty. | `string` | `''` |
| `min` | Min selectable date (forwarded to the calendar). Ignored in `time` mode. | `Date \| undefined` | `undefined` |
| `max` | Max selectable date (forwarded to the calendar). Ignored in `time` mode. | `Date \| undefined` | `undefined` |
| `dateFilter` | Predicate to disable specific dates (forwarded to the calendar). Ignored in `time` mode. | `((date: Date) => boolean) \| null` | `null` |
| `timeFormat` | Time-panel 12 / 24-hour format. Applies in `time` + `datetime` modes. | `'auto' \| '12h' \| '24h'` | `'auto'` |
| `showSeconds` | Render the seconds column. Applies in `time` + `datetime` modes. | `boolean` | `false` |
| `step` | Minute / second step for the time panel. | `number` | `1` |
| `disabled` | Disable interaction. Bound automatically from the field's disabled state when used with `[formField]`. | `boolean` | `false` |
| `readonly` | Read-only — input not typeable, but the trigger icon still opens the overlay. | `boolean` | `false` |
| `value` | The picked Date. Bound by `[formField]`, or two-way via `[(value)]`. | `Date \| null` | `null` |
| `(touch)` | Emitted on blur so a bound field can mark itself touched. | `void` | — |
| `ariaLabel` | Accessible 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 \| null` | `null` |
| `panelAriaLabel` | Accessible 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 \| null` | `null` |

## Range picker API

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `value` | The picked range as `[start, end]`. Either end may be `null` while half-picked; out-of-order ends are swapped on commit. | `WrDateRange \| null` | `null` |
| `mode` | `date` (default) renders a range calendar; `datetime` adds a time stepper per end. | `'date' \| 'datetime'` | `'date'` |
| `format` | Display + parse format for both ends. When omitted, derived from `mode` (`shortDate` / `shortDateTime`). | `WrDateFormat \| string \| null` | `null` |
| `startPlaceholder` | Placeholder for the start input. | `string` | `''` |
| `endPlaceholder` | Placeholder for the end input. | `string` | `''` |
| `separator` | Glyph rendered between the two inputs. | `string` | `'–'` |
| `minDate` | Earliest selectable date, both ends. Named `minDate` because signal forms reserve `min` for the value type — here a range. | `Date \| null` | `null` |
| `maxDate` | Latest selectable date, both ends. | `Date \| null` | `null` |
| `dateFilter` | Predicate disabling individual dates. | `((date: Date) => boolean) \| null` | `null` |
| `timeFormat` | Time-panel 12 / 24-hour format. Applies in `datetime` mode. | `'auto' \| '12h' \| '24h'` | `'auto'` |
| `showSeconds` | Render the seconds column on both time panels. | `boolean` | `false` |
| `step` | Minute / second step for the time panels. | `number` | `1` |
| `disabled` | Block interaction. | `boolean` | `false` |
| `readonly` | Inputs are not typeable; trigger icon still opens the overlay. | `boolean` | `false` |

## 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.

| Variable | Default | Declared on |
| --- | --- | --- |
| `--wr-time-picker-bg` | `var(--wr-color-surface)` | `.wr-time-picker` |
| `--wr-time-picker-border` | `var(--wr-color-outline)` | `.wr-time-picker` |
| `--wr-time-picker-col-width` | `3rem` | `.wr-time-picker` |
| `--wr-time-picker-color` | `var(--wr-color-on-surface)` | `.wr-time-picker` |
| `--wr-time-picker-muted` | `var(--wr-color-on-surface-muted)` | `.wr-time-picker` |
