Form
Calendar
Usage#
import { WdsCalendarComponent } from '@wds/angular/calendar';Examples#
Default#
wds-change with detail.value, an ISO date (2026-09-24). A click on the caption switches to months and years.
import { Component } from '@angular/core';
import { WdsCalendarComponent } from '@wds/angular/calendar';
@Component({
selector: 'app-calendar-default-demo',
imports: [WdsCalendarComponent],
template: `
<wds-calendar class="rounded-lg border border-border shadow-xs"></wds-calendar>
`,
})
export class CalendarDefaultDemo {}Range#
mode="range": the first click sets start, the second one end; after the first click the range follows the pointer. months="2" shows two months side by side.
From — to —
import { Component, signal } from '@angular/core';
import { WdsCalendarComponent } from '@wds/angular/calendar';
import type { WdsCalendarChangeDetail } from '@wds/core/calendar';
@Component({
selector: 'app-calendar-range-demo',
imports: [WdsCalendarComponent],
template: `
<div class="flex flex-col items-center gap-3">
<wds-calendar
class="rounded-lg border border-border shadow-xs"
mode="range"
months="2"
[start]="start()"
[end]="end()"
(wdsChange)="select($event)"
/>
<p class="m-0 text-sm text-muted-foreground">From {{ start() || '—' }} to {{ end() || '—' }}</p>
</div>
`,
})
export class CalendarRangeDemo {
protected readonly start = signal('');
protected readonly end = signal('');
protected select({ start, end }: WdsCalendarChangeDetail): void {
this.start.set(start);
this.end.set(end);
}
}Multiple#
mode="multiple" toggles each clicked day; the dates are in values.
import { Component } from '@angular/core';
import { WdsCalendarComponent } from '@wds/angular/calendar';
@Component({
selector: 'app-calendar-multiple-demo',
imports: [WdsCalendarComponent],
template: `
<wds-calendar class="rounded-lg border border-border shadow-xs" mode="multiple"></wds-calendar>
`,
})
export class CalendarMultipleDemo {}Presets#
value, and month shows the month of that date.
import { Component, signal } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsCalendarComponent } from '@wds/angular/calendar';
/** An ISO date `days` from today (local time). */
function isoFromToday(days: number): string {
const d = new Date();
d.setDate(d.getDate() + days);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
@Component({
selector: 'app-calendar-presets-demo',
imports: [WdsCalendarComponent, WdsButtonComponent],
template: `
<div class="flex flex-col items-center gap-3">
<div class="flex flex-wrap justify-center gap-2">
@for (preset of presets; track preset.label) {
<wds-button variant="outline" size="sm" (click)="value.set(preset.value)">{{ preset.label }}</wds-button>
}
</div>
<!-- month shows the month of the chosen date. -->
<wds-calendar class="rounded-lg border border-border shadow-xs" [value]="value()" [month]="value().slice(0, 7)" (wdsChange)="value.set($event.value)" />
</div>
`,
})
export class CalendarPresetsDemo {
protected readonly presets = [
{ label: 'Today', value: isoFromToday(0) },
{ label: 'Tomorrow', value: isoFromToday(1) },
{ label: 'In 3 days', value: isoFromToday(3) },
{ label: 'In a week', value: isoFromToday(7) },
{ label: 'In 2 weeks', value: isoFromToday(14) },
];
protected readonly value = signal(isoFromToday(0));
}Unavailable dates#
min and max limit the dates (the arrows stop at them), disabled-days blocks days of the week (here weekends) and disabledDates single dates, which are crossed out. For rules from an API use isDateDisabled.
import { Component } from '@angular/core';
import { WdsCalendarComponent } from '@wds/angular/calendar';
/** An ISO date `days` from today (local time). */
function isoFromToday(days: number): string {
const d = new Date();
d.setDate(d.getDate() + days);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
@Component({
selector: 'app-calendar-unavailable-demo',
imports: [WdsCalendarComponent],
template: `
<wds-calendar
class="rounded-lg border border-border shadow-xs"
mode="range"
disabled-days="0 6"
[min]="today"
[max]="lastDay"
[disabledDates]="booked"
/>
`,
})
export class CalendarUnavailableDemo {
protected readonly today = isoFromToday(0);
protected readonly lastDay = isoFromToday(60);
/** Booked dates are crossed out. */
protected readonly booked = [5, 6, 7, 12, 13, 20].map(isoFromToday);
}Day info#
dayInfo returns extra content for each day: text under the number (here a nightly price), dot and label, which is added to the name of the day for screen readers (by default the text). Give the cells room with --wds-calendar-cell-size.
Select check-in and check-out.
import { Component, computed, signal } from '@angular/core';
import { WdsCalendarComponent } from '@wds/angular/calendar';
import type { WdsCalendarDayInfo } from '@wds/core/calendar';
/** Nightly price: weekends cost more; a dot marks the last rooms. */
function priceOf(date: string): WdsCalendarDayInfo {
const d = new Date(`${date}T00:00`);
const weekend = d.getDay() === 5 || d.getDay() === 6;
const price = weekend ? 120 : 100;
const lastRooms = d.getDate() % 9 === 0;
return { text: `$${price}`, label: `$${price} per night${lastRooms ? ', last rooms' : ''}`, dot: lastRooms };
}
@Component({
selector: 'app-calendar-day-info-demo',
imports: [WdsCalendarComponent],
template: `
<div class="flex flex-col items-center gap-3">
<wds-calendar
class="rounded-lg border border-border shadow-xs [--wds-calendar-cell-size:2.75rem]"
mode="range"
[dayInfo]="priceOf"
[start]="start()"
[end]="end()"
(wdsChange)="start.set($event.start); end.set($event.end)"
/>
<p class="m-0 text-sm text-muted-foreground">{{ total() }}</p>
</div>
`,
})
export class CalendarDayInfoDemo {
protected readonly priceOf = priceOf;
protected readonly start = signal('');
protected readonly end = signal('');
/** Sum of the nights from start to the day before end. */
protected readonly total = computed(() => {
if (!this.start() || !this.end()) return 'Select check-in and check-out.';
let sum = 0;
let nights = 0;
for (const d = new Date(`${this.start()}T00:00`); d < new Date(`${this.end()}T00:00`); d.setDate(d.getDate() + 1)) {
const iso = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
sum += Number(priceOf(iso).text!.slice(1));
nights++;
}
return `${nights} ${nights === 1 ? 'night' : 'nights'}: $${sum}`;
});
}Month and year#
view="month" selects a month (2026-09) and view="year" a year (2026).
import { Component } from '@angular/core';
import { WdsCalendarComponent } from '@wds/angular/calendar';
@Component({
selector: 'app-calendar-month-and-year-demo',
imports: [WdsCalendarComponent],
template: `
<div class="flex flex-wrap items-start justify-center gap-4">
<wds-calendar class="rounded-lg border border-border shadow-xs" view="month"></wds-calendar>
<wds-calendar class="rounded-lg border border-border shadow-xs" view="year"></wds-calendar>
</div>
`,
})
export class CalendarMonthAndYearDemo {}Week numbers and locale#
show-week adds week numbers. The names and the first day of the week follow the nearest lang attribute, or locale: in English the week starts on Sunday, in German on Monday.
import { Component } from '@angular/core';
import { WdsCalendarComponent } from '@wds/angular/calendar';
@Component({
selector: 'app-calendar-week-numbers-and-locale-demo',
imports: [WdsCalendarComponent],
template: `
<div class="flex flex-wrap items-start justify-center gap-4">
<wds-calendar class="rounded-lg border border-border shadow-xs" show-week></wds-calendar>
<wds-calendar class="rounded-lg border border-border shadow-xs" locale="de-DE" show-week></wds-calendar>
</div>
`,
})
export class CalendarWeekNumbersAndLocaleDemo {}Cell size#
--wds-calendar-cell-size and --wds-calendar-cell-radius change the size and shape of the cells.
import { Component } from '@angular/core';
import { WdsCalendarComponent } from '@wds/angular/calendar';
@Component({
selector: 'app-calendar-cell-size-demo',
imports: [WdsCalendarComponent],
template: `
<wds-calendar class="rounded-lg border border-border shadow-xs [--wds-calendar-cell-size:2.75rem] [--wds-calendar-cell-radius:9999px]"></wds-calendar>
`,
})
export class CalendarCellSizeDemo {}Disabled#
import { Component } from '@angular/core';
import { WdsCalendarComponent } from '@wds/angular/calendar';
@Component({
selector: 'app-calendar-disabled-demo',
imports: [WdsCalendarComponent],
template: `
<wds-calendar class="rounded-lg border border-border shadow-xs" disabled></wds-calendar>
`,
})
export class CalendarDisabledDemo {}Forms#
Integration with Angular forms. Pick the API your app uses.
Signal forms#
WdsDatePickerField connects wds-calendar to [formField] (a string); for mode="multiple" use WdsDatePickerMultipleField (string[]) and for mode="range" WdsDatePickerRangeField ({ start, end }). wds-field-set names the calendar with its legend.
import { Component, computed, signal } from '@angular/core';
import { FormField, FormRoot, form, required } from '@angular/forms/signals';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsCalendarComponent } from '@wds/angular/calendar';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsDatePickerField } from '@wds/angular/forms';
@Component({
selector: 'app-calendar-signal-forms-demo',
imports: [WdsCalendarComponent, ...WDS_FIELD, FormField, FormRoot, WdsButtonComponent, WdsDatePickerField],
template: `
<form class="flex flex-col items-start gap-4" [formRoot]="deliveryForm">
<wds-field-set>
<wds-field-legend>Delivery date</wds-field-legend>
<wds-calendar class="rounded-lg border border-border shadow-xs" [formField]="deliveryForm.date" />
<wds-field-error [errors]="showDateError() ? deliveryForm.date().errors() : []" />
</wds-field-set>
<wds-button type="submit">Submit</wds-button>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class CalendarSignalFormsDemo {
protected readonly delivery = signal({ date: '' });
protected readonly submitted = signal('—');
protected readonly deliveryForm = form(
this.delivery,
(path) => {
required(path.date, { message: 'Select a delivery date.' });
},
{
submission: {
action: async (form) => {
this.submitted.set(JSON.stringify(form().value()));
},
},
}
);
/** The error appears after the user leaves the calendar or submits the form. */
protected readonly showDateError = computed(() => this.deliveryForm.date().touched() && this.deliveryForm.date().invalid());
}Template-driven#
WdsDatePickerValueAccessor makes ngModel work on wds-calendar; the value type follows mode.
import { Component, signal } from '@angular/core';
import { FormsModule, type NgForm } from '@angular/forms';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsCalendarComponent } from '@wds/angular/calendar';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsDatePickerValueAccessor } from '@wds/angular/forms';
@Component({
selector: 'app-calendar-template-driven-demo',
imports: [WdsCalendarComponent, ...WDS_FIELD, FormsModule, WdsButtonComponent, WdsDatePickerValueAccessor],
template: `
<form #deliveryForm="ngForm" class="flex flex-col items-start gap-4" (ngSubmit)="save(deliveryForm)">
<wds-field-set>
<wds-field-legend>Delivery date</wds-field-legend>
<wds-calendar #date="ngModel" class="rounded-lg border border-border shadow-xs" name="date" [(ngModel)]="delivery.date" required />
@if (date.invalid && date.touched) {
<wds-field-error>Select a delivery date.</wds-field-error>
}
</wds-field-set>
<wds-button type="submit">Submit</wds-button>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class CalendarTemplateDrivenDemo {
protected readonly delivery = { date: '' };
protected readonly submitted = signal('—');
protected save(form: NgForm): void {
if (form.invalid) {
form.form.markAllAsTouched();
return;
}
this.submitted.set(JSON.stringify(form.value));
}
}Reactive forms#
WdsDatePickerValueAccessor works with formControl and formControlName.
import { Component, signal } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsCalendarComponent } from '@wds/angular/calendar';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsDatePickerValueAccessor } from '@wds/angular/forms';
@Component({
selector: 'app-calendar-reactive-forms-demo',
imports: [WdsCalendarComponent, ...WDS_FIELD, ReactiveFormsModule, WdsButtonComponent, WdsDatePickerValueAccessor],
template: `
<form class="flex flex-col items-start gap-4" [formGroup]="deliveryForm" (ngSubmit)="save()">
<wds-field-set>
<wds-field-legend>Delivery date</wds-field-legend>
<wds-calendar class="rounded-lg border border-border shadow-xs" formControlName="date" />
@if (date.invalid && date.touched) {
<wds-field-error>Select a delivery date.</wds-field-error>
}
</wds-field-set>
<wds-button type="submit">Submit</wds-button>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class CalendarReactiveFormsDemo {
protected readonly deliveryForm = new FormGroup({
date: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
});
protected readonly date = this.deliveryForm.controls.date;
protected readonly submitted = signal('—');
protected save(): void {
if (this.deliveryForm.invalid) {
this.deliveryForm.markAllAsTouched();
return;
}
this.submitted.set(JSON.stringify(this.deliveryForm.getRawValue()));
}
}API References#
wds-calendar#
2026-09-24).
Keyboard (the WAI-ARIA Date Picker pattern): arrows – day/week (in months and years: a grid cell),
Home/End – start/end of the week, PageUp/PageDown – previous/next month (with Shift – year),
Enter/Space – select, Escape – back from months/years to the previous level.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| mode | WdsCalendarMode | 'single' | single – one date, multiple – several dates, range – a range from–to. |
| view | WdsCalendarView | 'date' | date (a day), month (2026-09) or year (2026). |
| value | string | '' | single mode: the selected date. |
| values | string[] | multiple mode: the selected dates. In HTML a space-separated list. | |
| start | string | '' | range mode: the start of the range. |
| end | string | '' | range mode: the end of the range. |
| min | string | '' | |
| max | string | '' | |
| disabled-dates.disabledDates | string[] | ||
| disabled-days.disabledDays | number[] | [] | disabled-days="0 6". |
| months | number | 1 | |
| month | string | '' | 2026-09). Defaults to the month of the selected date, or the current one. |
| first-day.firstDay | number | undefined | ||
| show-week.showWeek | boolean | false | |
| hide-outside-days.hideOutsideDays | boolean | false | |
| locale | string | '' | en-US). Defaults to the nearest lang attribute. |
| disabled | boolean | false |
Properties
| Property | Description |
|---|---|
| isDateDisabled((date: string) => boolean) | null | view level. |
| dayInfo((date: string) => WdsCalendarDayInfo | string | null | undefined) | null | { text }. Give the cells room with --wds-calendar-cell-size. |
Events
| Event | Description |
|---|---|
| wds-month-changeCustomEvent<WdsCalendarMonthChangeDetail> | |
| wds-changeCustomEvent<WdsCalendarChangeDetail> |
Methods
| Method | Description |
|---|---|
| focus(options?: FocusOptions) | |
| goTo(date: string) | |
| resetView() | view). |
CSS parts (::part)
| Part | Description |
|---|---|
| base | |
| header | |
| caption | |
| nav-button | |
| grid | |
| day | |
| day-info | dayInfo). |
| day-dot | dayInfo). |
CSS custom properties
| Property | Description |
|---|---|
| --wds-calendar-cell-size | |
| --wds-calendar-cell-radius | |
| --wds-calendar-focus |
Accessibility#
Screen reader#
role="grid" named by the month and year. Days are cells with the full date as their name, aria-selected (also for days inside a range), aria-disabled for unavailable days and aria-current="date" for today.
wds-field-set legend, as in the forms examples.
Keyboard support#
| Key | Function |
|---|---|