bpdm/ui
Form

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.

date.tsx
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)} />;
}
date.component.ts
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.

range.tsx
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"
    />
  );
}
range.component.ts
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.

range-presets.tsx
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"
    />
  );
}
range-presets.component.ts
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.

constraints.tsx
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"
    />
  );
}
constraints.component.ts
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.

June 2026
Mon
Tue
Wed
Thu
Fri
Sat
Sun
inline.tsx
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)} />;
}
inline.component.ts
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;
}

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

Mon
Tue
Wed
Thu
Fri
Sat
Sun
caption.tsx
import { Calendar } from '@bpdm/ui/calendar';

export function Caption() {
  return <Calendar captionLayout="dropdown" />;
}
caption.component.ts
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.

June 2026
Mon
Tue
Wed
Thu
Fri
Sat
Sun
square.tsx
import { Calendar } from '@bpdm/ui/calendar';

export function Square() {
  return <Calendar dayShape="square" />;
}
square.component.ts
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).

sunday-first.tsx
import { DatePicker } from '@bpdm/ui/calendar';

export function SundayFirst() {
  return <DatePicker weekStartsOn={0} placeholder="Pick a date" />;
}
sunday-first.component.ts
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.

invalid.tsx
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>
  );
}
invalid.component.ts
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).

confirm.tsx
import { DatePicker, defaultRangePresets } from '@bpdm/ui/calendar';

export function Confirm() {
  return (
    <DatePicker
      mode="range"
      presets={defaultRangePresets}
      confirm
      placeholder="Pick a date range"
    />
  );
}
confirm.component.ts
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.

June 2026
Mon
Tue
Wed
Thu
Fri
Sat
Sun

Applied: 6/15/2026

inline-confirm.tsx
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>
  );
}
inline-confirm.component.ts
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.

booking.tsx
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"
    />
  );
}
booking.component.ts
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

PropTypeDefaultDescription
modesingle | rangesinglePick a Date or a { from, to } range.
value / defaultValueDate | DateRange | nullControlled / uncontrolled selection.
onChange (React)(value) => voidFires on selection. Angular uses two-way [(value)].
min / maxDateSelectable bounds (out-of-range days are disabled).
disabled(date: Date) => booleanDisable specific days (e.g. weekends).
weekStartsOn0 | 11First column — 0 Sunday, 1 Monday.
dayShapecircle | squarecircleDay highlight shape.
numberOfMonthsnumber1Months shown side by side.
captionLayoutbuttons | dropdownbuttonsHeader: prev/next only, or month + year menus.
fromYear / toYearnumber−100y / +10yYear-dropdown range.
localestring (BCP 47)runtime defaultMonth / weekday names + date formatting.
messages{ calendar, previousMonth, nextMonth, month, year, clear, cancel, apply }EnglishOverride control labels for i18n.
className (React) / class (Angular)stringExtra classes.

DatePicker / bpdm-date-picker — adds

PropTypeDefaultDescription
presetsDateRangePreset[]Range-mode quick presets (e.g. defaultRangePresets).
placeholderstringPick a dateTrigger text when empty.
clearablebooleantrueShow a clear (×) button when a value is set.
confirmbooleanfalseBuffer the selection; commit only on Apply (adds a Cancel/Apply footer).
disabledInputbooleanfalseDisable the whole trigger. Not the same as disabled (which filters days).
invalidbooleanfalsearia-invalid styling on the trigger.
idstringTrigger id (for <label> association).
className (React) / class (Angular)stringExtra classes on the trigger.
contentClassNamestringExtra 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 as role="columnheader". Each day button has a full, locale-formatted accessible name (e.g. "January 15, 2026", not just "15"), today carries aria-current="date", the selected day's cell is aria-selected, and range mode sets aria-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 DatePicker trigger 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: locale drives month + weekday names and the day/trigger date formatting through Intl.DateTimeFormat — no hard-coded English. Set weekStartsOn for the first day of the week. The fixed control labels (prev/next month, month, year, clear) are overridable via messages.
  • 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).
calendar-i18n.tsx
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} />
  );
}
calendar-i18n.component.ts
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',
  };
}

On this page