<!-- Protocore Design System — Calendar -->
# Calendar

- **Category:** Inputs (`inputs`)
- **Slug:** `inputs/calendar`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { Calendar } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/inputs/calendar

> Month-grid date picker with full keyboard navigation, single or range selection, and locale-aware labels.

## When to use it

Use **Calendar** when the month grid should stay on screen — inline scheduling, a booking sidebar, a report window. When the calendar should live behind a field, use **DatePicker** (single) or **DateRangePicker** (range), both of which wrap this same grid in a popover. When users know the exact date and would rather type it, offer **DateInput** as the keyboard-first alternative.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `captionLayout` | `enum` | no | `label` | Header layout: `label` (static caption) or `dropdown` (month + year Selects). |
| `className` | `string` | no | — | — |
| `defaultMonth` | `Date` | no | — | Initial visible month in uncontrolled mode. |
| `defaultRangeValue` | `DateRange` | no | — | Initial selected range in uncontrolled range mode. |
| `defaultValue` | `Date \| null` | no | — | Initial selected date in uncontrolled single mode. |
| `isDateDisabled` | `((date: Date) => boolean)` | no | — | Predicate to disable arbitrary days (e.g. weekends, holidays). |
| `locale` | `string` | no | — | BCP-47 locale for month, weekday, and day-cell labels. Defaults to the runtime locale. |
| `max` | `Date` | no | — | Latest selectable day (inclusive); later days are disabled. |
| `min` | `Date` | no | — | Earliest selectable day (inclusive); earlier days are disabled. |
| `month` | `Date` | no | — | Controlled first visible month (any day within it). |
| `numberOfMonths` | `number` | no | `1` | Number of consecutive month grids to render side by side. |
| `onMonthChange` | `((month: Date) => void)` | no | — | Fires when the visible month changes via navigation. |
| `onRangeValueChange` | `((range: DateRange) => void)` | no | — | Fires with the ordered `{ start, end }` as the range is built (range mode). |
| `onValueChange` | `((date: Date) => void)` | no | — | Fires with the newly selected day (single mode). |
| `range` | `boolean` | no | `false` | Switch to range selection — pairs with `rangeValue` / `onRangeValueChange`. |
| `rangeValue` | `DateRange` | no | — | Controlled selected range (range mode). |
| `style` | `CSSProperties` | no | — | — |
| `value` | `Date \| null` | no | — | Controlled selected date (single mode). Pass `null` for no selection. |
| `weekStartsOn` | `enum` | no | `1` | First column of the week: 0 = Sunday … 6 = Saturday. |
| `yearRange` | `number` | no | `10` | Range of years offered by the year dropdown, relative to the view year. |

## Examples

### Basics

A single-date calendar. Controllable via `value` / `defaultValue` / `onValueChange`; the visible month is controllable too via `month` / `defaultMonth`. Weeks start on Monday by default — set `weekStartsOn` to change it.

```tsx
import { useState } from "react";
import { Calendar } from "@protocore/pds";

export default function Basics() {
  // Controllable single-date grid. Arrow keys move by day, Enter selects.
  const [value, setValue] = useState<Date | null>(new Date(2026, 6, 6));

  return (
    <div style={{ display: "grid", gap: 12, justifyItems: "start" }}>
      <Calendar value={value} onValueChange={setValue} locale="en-GB" />
      <span style={{ font: "13px system-ui", opacity: 0.7 }}>
        {value ? `Selected: ${value.toDateString()}` : "No date selected"}
      </span>
    </div>
  );
}
```

### Min / max & disabled days

Bound the selectable window with `min` and `max`; disable arbitrary days (weekends, blackout dates) with `isDateDisabled`. Out-of-range days are non-interactive and marked `aria-disabled`.

```tsx
import { useState } from "react";
import { Calendar } from "@protocore/pds";

export default function Bounded() {
  // A booking window: only the next fortnight is selectable, and weekends are
  // blacked out via isDateDisabled.
  const today = new Date(2026, 6, 6);
  const min = today;
  const max = new Date(2026, 6, 20);
  const [value, setValue] = useState<Date | null>(null);

  const noWeekends = (d: Date) => d.getDay() === 0 || d.getDay() === 6;

  return (
    <Calendar
      value={value}
      onValueChange={setValue}
      defaultMonth={today}
      min={min}
      max={max}
      isDateDisabled={noWeekends}
      locale="en-GB"
    />
  );
}
```

## Do & don't

**Do**

- Let users reach any date by keyboard — arrows move by day, PageUp/PageDown by month.
- Set `min` / `max` to the real booking window instead of validating after selection.
- Pick a `weekStartsOn` that matches your audience (Monday for most of Europe).
- Pass a `locale` when the surrounding UI is localised so weekday and month names match.

**Don't**

- Don't use a Calendar for a far-past date like a birth year without `captionLayout="dropdown"`.
- Don't reimplement range logic on top of single mode — pass `range` and read `onRangeValueChange`.
- Don't colour-code availability with hue alone; pair it with a disabled state or label.
- Don't trap focus inside the grid — it is a composable region, not a modal.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Arrow keys` | Move the focused day left/right by a day, up/down by a week. |
| `PageUp / PageDown` | Move to the same day of the previous / next month. |
| `Shift + PageUp / PageDown` | Move by a whole year. |
| `Home / End` | Jump to the first / last day of the current week. |
| `Enter / Space` | Select the focused day. |

**Notes**

- The grid uses `role="grid"` with `gridcell` children; the selected cell carries `aria-selected` and today is marked `aria-current="date"`.
- A roving tabindex keeps exactly one day in the tab order, so Tab enters and leaves the grid in a single step.
- Each day exposes a full accessible name (e.g. "Monday, July 6, 2026") built with Intl.DateTimeFormat in the active locale.
- Weekday headers are `columnheader`s; out-of-range days are `aria-disabled` rather than removed, preserving the grid shape.

## Related

`date-picker`, `date-input`, `date-range-picker`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
