Calendar
Date & range picker built on native dates — inline calendar or popover picker, presets, min/max and disabled days, in React (@bpdm/ui) and Angular (@bpdm/ng).
The Calendar is a date and range picker built on native Dates — no date library. DatePicker
shows a trigger that opens a Calendar in a popover; Calendar can also be used inline. It handles
a single date or a range, month/year navigation, min/max and per-day disabled, today +
selection highlights, and full keyboard support (arrows move, Enter selects, PageUp/PageDown change
month). In React they're <DatePicker> / <Calendar>; in Angular, <bpdm-date-picker> /
<bpdm-calendar>.
Usage
DatePicker is the common case — a trigger showing the value that opens the calendar. It's
clearable and closes on pick.
import { useState } from 'react';
import { DatePicker } from '@bpdm/ui/calendar';
export function PickDate() {
const [date, setDate] = useState<Date | null>(null);
return <DatePicker value={date} onChange={(v) => setDate(v as Date | null)} />;
}import { Component } from '@angular/core';
import { BpdmDatePicker } from '@bpdm/ng';
@Component({
selector: 'app-date',
standalone: true,
imports: [BpdmDatePicker],
template: `<bpdm-date-picker [(value)]="date" />`,
})
export class DateComponent {
date: Date | null = null;
}Date range
Set mode="range" for a from–to selection — it shows two months and closes once both ends are
picked.
import { useState } from 'react';
import { DatePicker, type DateRange } from '@bpdm/ui/calendar';
export function PickRange() {
const [range, setRange] = useState<DateRange | null>(null);
return (
<DatePicker
mode="range"
value={range}
onChange={(v) => setRange(v as DateRange | null)}
placeholder="Pick a date range"
/>
);
}import { Component } from '@angular/core';
import { BpdmDatePicker, type DateRange } from '@bpdm/ng';
@Component({
selector: 'app-range',
standalone: true,
imports: [BpdmDatePicker],
template: `<bpdm-date-picker mode="range" [(value)]="range" placeholder="Pick a date range" />`,
})
export class RangeComponent {
range: DateRange | null = null;
}Range with presets
Pass presets for quick ranges — defaultRangePresets covers the common dashboard ones, or supply
your own { label, range() } list (the range is computed at click time). Pair with
captionLayout="dropdown" to jump across months and years fast.
import { useState } from 'react';
import { DatePicker, defaultRangePresets, type DateRange } from '@bpdm/ui/calendar';
// presets are just { label, range() } — configure any you want
const presets = [
{
label: 'Year to date',
range: () => {
const n = new Date();
return { from: new Date(n.getFullYear(), 0, 1), to: n };
},
},
{
label: 'Last quarter',
range: () => {
const n = new Date();
return { from: new Date(n.getFullYear(), n.getMonth() - 3, 1), to: n };
},
},
...defaultRangePresets, // Last 7 days, Last 30 days, This month, This year, Previous year
];
export function RangeWithPresets() {
const [range, setRange] = useState<DateRange | null>(null);
return (
<DatePicker
mode="range"
value={range}
onChange={(v) => setRange(v as DateRange | null)}
presets={presets}
captionLayout="dropdown"
placeholder="Pick a date range"
/>
);
}import { Component } from '@angular/core';
import { BpdmDatePicker, defaultRangePresets, type DateRange } from '@bpdm/ng';
@Component({
selector: 'app-range-presets',
standalone: true,
imports: [BpdmDatePicker],
template: `
<bpdm-date-picker
mode="range"
[(value)]="range"
[presets]="presets"
captionLayout="dropdown"
placeholder="Pick a date range"
/>
`,
})
export class RangePresetsComponent {
range: DateRange | null = null;
presets = [
{
label: 'Year to date',
range: () => {
const n = new Date();
return { from: new Date(n.getFullYear(), 0, 1), to: n };
},
},
{
label: 'Last quarter',
range: () => {
const n = new Date();
return { from: new Date(n.getFullYear(), n.getMonth() - 3, 1), to: n };
},
},
...defaultRangePresets,
];
}Min, max & disabled days
Clamp the selectable window with min / max, and block specific days with a disabled predicate
— here, only weekdays in the next three months are pickable.
import { useState } from 'react';
import { DatePicker } from '@bpdm/ui/calendar';
export function Constraints() {
const [date, setDate] = useState<Date | null>(null);
const today = new Date();
const inThreeMonths = new Date(today.getFullYear(), today.getMonth() + 3, today.getDate());
return (
<DatePicker
value={date}
onChange={(v) => setDate(v as Date | null)}
min={today}
max={inThreeMonths}
disabled={(d) => d.getDay() === 0 || d.getDay() === 6}
placeholder="Weekday in next 3 months"
/>
);
}import { Component } from '@angular/core';
import { BpdmDatePicker } from '@bpdm/ng';
@Component({
selector: 'app-constraints',
standalone: true,
imports: [BpdmDatePicker],
template: `
<bpdm-date-picker
[(value)]="date"
[min]="today"
[max]="inThreeMonths"
[disabled]="isWeekend"
placeholder="Weekday in next 3 months" />
`,
})
export class ConstraintsComponent {
date: Date | null = null;
today = new Date();
inThreeMonths = new Date(this.today.getFullYear(), this.today.getMonth() + 3, this.today.getDate());
isWeekend = (d: Date) => d.getDay() === 0 || d.getDay() === 6;
}Inline calendar
Use Calendar directly (without the popover) to embed it in a page or panel.
import { useState } from 'react';
import { Calendar } from '@bpdm/ui/calendar';
export function Inline() {
const [date, setDate] = useState<Date | null>(null);
return <Calendar value={date} onChange={(v) => setDate(v as Date | null)} />;
}import { Component } from '@angular/core';
import { BpdmCalendar } from '@bpdm/ng';
@Component({
selector: 'app-inline',
standalone: true,
imports: [BpdmCalendar],
template: `<bpdm-calendar [(value)]="date" />`,
})
export class InlineComponent {
date: Date | null = null;
}Dropdown caption
captionLayout="dropdown" swaps the prev/next header for month + year menus — handy for jumping
across years. (See also numberOfMonths for side-by-side months and weekStartsOn for the first
day of the week.)
import { Calendar } from '@bpdm/ui/calendar';
export function Caption() {
return <Calendar captionLayout="dropdown" />;
}import { Component } from '@angular/core';
import { BpdmCalendar } from '@bpdm/ng';
@Component({
selector: 'app-caption',
standalone: true,
imports: [BpdmCalendar],
template: `<bpdm-calendar captionLayout="dropdown" />`,
})
export class CaptionComponent {}Square days
dayShape="square" swaps the circular day highlight for a rounded-corner square.
import { Calendar } from '@bpdm/ui/calendar';
export function Square() {
return <Calendar dayShape="square" />;
}import { Component } from '@angular/core';
import { BpdmCalendar } from '@bpdm/ng';
@Component({
selector: 'app-square',
standalone: true,
imports: [BpdmCalendar],
template: `<bpdm-calendar dayShape="square" />`,
})
export class SquareComponent {}Week starts on Sunday
weekStartsOn={0} starts the week on Sunday instead of Monday (locale conventions differ).
import { DatePicker } from '@bpdm/ui/calendar';
export function SundayFirst() {
return <DatePicker weekStartsOn={0} placeholder="Pick a date" />;
}import { Component } from '@angular/core';
import { BpdmDatePicker } from '@bpdm/ng';
@Component({
selector: 'app-sunday-first',
standalone: true,
imports: [BpdmDatePicker],
template: `<bpdm-date-picker [weekStartsOn]="0" placeholder="Pick a date" />`,
})
export class SundayFirstComponent {}Invalid
Mark the picker invalid for a destructive border, and pair it with an error message.
Please choose a date.
import { DatePicker } from '@bpdm/ui/calendar';
export function Invalid() {
return (
<div className="flex flex-col gap-1.5">
<DatePicker invalid placeholder="Required" />
<p className="text-sm text-destructive">Please choose a date.</p>
</div>
);
}import { Component } from '@angular/core';
import { BpdmDatePicker } from '@bpdm/ng';
@Component({
selector: 'app-invalid',
standalone: true,
imports: [BpdmDatePicker],
template: `
<div class="flex flex-col gap-1.5">
<bpdm-date-picker invalid placeholder="Required" />
<p class="text-sm text-destructive">Please choose a date.</p>
</div>
`,
})
export class InvalidComponent {}Confirm before applying
By default a pick commits immediately. Pass confirm to buffer the selection and
commit only on Apply — Cancel, Escape and outside-click discard the draft. Ideal when
applying triggers an expensive action (dashboards, reports, saved filters).
import { DatePicker, defaultRangePresets } from '@bpdm/ui/calendar';
export function Confirm() {
return (
<DatePicker
mode="range"
presets={defaultRangePresets}
confirm
placeholder="Pick a date range"
/>
);
}import { Component } from '@angular/core';
import { BpdmDatePicker, defaultRangePresets, type DateRangePreset } from '@bpdm/ng';
@Component({
selector: 'app-confirm',
standalone: true,
imports: [BpdmDatePicker],
template: `
<bpdm-date-picker
mode="range"
[presets]="presets"
confirm
placeholder="Pick a date range"
/>
`,
})
export class ConfirmComponent {
presets: DateRangePreset[] = defaultRangePresets;
}Inline, with your own actions
Calendar is fully controlled, so an inline (non-popover) Apply/Cancel flow needs no extra
props — hold a draft in your own state, render your own buttons, and commit on Apply.
Applied: 6/15/2026
import { useState } from 'react';
import { Calendar } from '@bpdm/ui/calendar';
import { Button } from '@bpdm/ui/button';
export function InlineConfirm() {
const [committed, setCommitted] = useState<Date | null>(null);
const [draft, setDraft] = useState<Date | null>(null);
return (
<div className="w-fit">
<Calendar value={draft} onChange={(v) => setDraft(v as Date | null)} />
<div className="mt-2 flex justify-end gap-2">
<Button variant="secondary" onClick={() => setDraft(committed)}>
Cancel
</Button>
<Button variant="primary" onClick={() => setCommitted(draft)}>
Apply
</Button>
</div>
</div>
);
}import { Component } from '@angular/core';
import { BpdmCalendar, BpdmButton } from '@bpdm/ng';
@Component({
selector: 'app-inline-confirm',
standalone: true,
imports: [BpdmCalendar, BpdmButton],
template: `
<div class="w-fit">
<bpdm-calendar [(value)]="draft" />
<div class="mt-2 flex justify-end gap-2">
<button bpdmButton variant="secondary" (click)="draft = committed">Cancel</button>
<button bpdmButton variant="primary" (click)="committed = draft">Apply</button>
</div>
</div>
`,
})
export class InlineConfirmComponent {
committed: Date | null = null;
draft: Date | null = null;
}Complete example
Everything on this page composed into one control: a range DatePicker with
quick-pick presets, a dropdown month/year caption, a Monday-first week, square
day cells, and weekends disabled.
import { DatePicker, defaultRangePresets } from '@bpdm/ui/calendar';
const isWeekend = (d: Date) => d.getDay() === 0 || d.getDay() === 6;
const presets = [
{
label: 'Year to date',
range: () => ({ from: new Date(new Date().getFullYear(), 0, 1), to: new Date() }),
},
{
label: 'Last quarter',
range: () => {
const now = new Date();
return { from: new Date(now.getFullYear(), now.getMonth() - 3, 1), to: now };
},
},
...defaultRangePresets,
];
export function Booking() {
return (
<DatePicker
mode="range"
presets={presets}
captionLayout="dropdown"
weekStartsOn={1}
dayShape="square"
disabled={isWeekend}
placeholder="Book a stay"
/>
);
}import { Component } from '@angular/core';
import { BpdmDatePicker, defaultRangePresets, type DateRangePreset } from '@bpdm/ng';
@Component({
selector: 'app-booking',
standalone: true,
imports: [BpdmDatePicker],
template: `
<bpdm-date-picker
mode="range"
[presets]="presets"
captionLayout="dropdown"
[weekStartsOn]="1"
dayShape="square"
[disabled]="isWeekend"
placeholder="Book a stay"
/>
`,
})
export class BookingComponent {
isWeekend = (d: Date) => d.getDay() === 0 || d.getDay() === 6;
presets: DateRangePreset[] = [
{
label: 'Year to date',
range: () => ({ from: new Date(new Date().getFullYear(), 0, 1), to: new Date() }),
},
{
label: 'Last quarter',
range: () => {
const now = new Date();
return { from: new Date(now.getFullYear(), now.getMonth() - 3, 1), to: now };
},
},
...defaultRangePresets,
];
}API
Calendar (React) / <bpdm-calendar> renders the month grid; DatePicker /
<bpdm-date-picker> wraps it in a popover trigger and adds the props in the second block.
Calendar / bpdm-calendar
| Prop | Type | Default | Description |
|---|---|---|---|
mode | single | range | single | Pick a Date or a { from, to } range. |
value / defaultValue | Date | DateRange | null | — | Controlled / uncontrolled selection. |
onChange (React) | (value) => void | — | Fires on selection. Angular uses two-way [(value)]. |
min / max | Date | — | Selectable bounds (out-of-range days are disabled). |
disabled | (date: Date) => boolean | — | Disable specific days (e.g. weekends). |
weekStartsOn | 0 | 1 | 1 | First column — 0 Sunday, 1 Monday. |
dayShape | circle | square | circle | Day highlight shape. |
numberOfMonths | number | 1 | Months shown side by side. |
captionLayout | buttons | dropdown | buttons | Header: prev/next only, or month + year menus. |
fromYear / toYear | number | −100y / +10y | Year-dropdown range. |
locale | string (BCP 47) | runtime default | Month / weekday names + date formatting. |
messages | { calendar, previousMonth, nextMonth, month, year, clear, cancel, apply } | English | Override control labels for i18n. |
className (React) / class (Angular) | string | — | Extra classes. |
DatePicker / bpdm-date-picker — adds
| Prop | Type | Default | Description |
|---|---|---|---|
presets | DateRangePreset[] | — | Range-mode quick presets (e.g. defaultRangePresets). |
placeholder | string | Pick a date | Trigger text when empty. |
clearable | boolean | true | Show a clear (×) button when a value is set. |
confirm | boolean | false | Buffer the selection; commit only on Apply (adds a Cancel/Apply footer). |
disabledInput | boolean | false | Disable the whole trigger. Not the same as disabled (which filters days). |
invalid | boolean | false | aria-invalid styling on the trigger. |
id | string | — | Trigger id (for <label> association). |
className (React) / class (Angular) | string | — | Extra classes on the trigger. |
contentClassName | string | — | Extra classes on the popover panel. |
`disabled` vs `disabledInput`
disabled is the per-day predicate inherited from Calendar — (date) => boolean — used to grey
out individual days. To disable the whole picker trigger, use disabledInput.
Accessibility
- Each month is a WAI-ARIA grid:
role="grid"→role="row"per week →role="gridcell", with the weekday headers asrole="columnheader". Each day button has a full, locale-formatted accessible name (e.g. "January 15, 2026", not just "15"), today carriesaria-current="date", the selected day's cell isaria-selected, and range mode setsaria-multiselectable. - Roving focus: real DOM focus moves onto the active day as you navigate, so a screen reader
announces each date — only the focused day is tabbable (roving
tabindex). - Keyboard: arrows move day-by-day and across weeks (RTL-aware — they follow the visual layout), Home/End jump to the first/last day of the week, PageUp/PageDown change month, Enter/Space select. The month/year dropdowns keep their own keys (they sit outside the grid), and nav buttons + dropdowns are labelled.
- The
DatePickertrigger shows the formatted value and opens the grid in a popover; the clear control is a real, keyboard-reachable labelled button.
Internationalization
- Locale-aware by design:
localedrives month + weekday names and the day/trigger date formatting throughIntl.DateTimeFormat— no hard-coded English. SetweekStartsOnfor the first day of the week. The fixed control labels (prev/next month, month, year, clear) are overridable viamessages. - RTL-safe: the day grid mirrors under
dir="rtl"(CSS grid follows writing direction), the prev/next chevrons flip, and the range band uses logical rounding (rounded-s/rounded-e).
import { Calendar } from '@bpdm/ui/calendar';
const de = {
calendar: 'Kalender',
previousMonth: 'Voriger Monat',
nextMonth: 'Nächster Monat',
month: 'Monat',
year: 'Jahr',
clear: 'Löschen',
};
export function GermanCalendar() {
return (
<Calendar locale="de-DE" weekStartsOn={1} captionLayout="dropdown" messages={de} />
);
}import { Component } from '@angular/core';
import { BpdmCalendar } from '@bpdm/ng';
@Component({
selector: 'app-german-calendar',
standalone: true,
imports: [BpdmCalendar],
template: `
<bpdm-calendar
locale="de-DE"
[weekStartsOn]="1"
captionLayout="dropdown"
[messages]="de"
/>
`,
})
export class GermanCalendarComponent {
de = {
calendar: 'Kalender',
previousMonth: 'Voriger Monat',
nextMonth: 'Nächster Monat',
month: 'Monat',
year: 'Jahr',
clear: 'Löschen',
};
}