# Calendar

A calendar for selecting a single date, several dates, or a range of dates.

```twig
<twig:Calendar
    mode="single"
    today="2026-03-15"
    selected="2026-03-15"
    captionLayout="dropdown"
    class="mx-auto rounded-lg border"
/>
```

## Installation

> [!NOTE]
> Available since UX Toolkit 3.5.

```shell
php bin/console ux:install calendar --kit shadcn
```
## Usage

```twig
<twig:Calendar
    mode="single | multiple | range"
    name="date"
    selected="2026-03-15"
    month="2026-03-01"
    captionLayout="label | dropdown"
/>
```

Dates are exchanged as `Y-m-d` strings. When `name` is set, the selection is mirrored into hidden inputs (`name`, `name[]` in `multiple` mode, `name[from]` and `name[to]` in `range` mode) so the calendar can be submitted with a form. Selecting a date also dispatches a `calendar:select` event, and navigating to another month dispatches a `calendar:month-change` event; both bubble, so an ancestor element can listen for them. This is the way to react to the selection without reading the hidden inputs.

## Examples

### Basic

A calendar with no initial selection. Use `class="rounded-lg border"` to frame it.

```twig
<twig:Calendar
    mode="single"
    today="2026-03-15"
    month="2026-03-01"
    class="mx-auto rounded-lg border"
/>
```

### Range Calendar

Use `mode="range"` to select a period. The first entry of `selected` is the start of the range, the second one its end.

```twig
<twig:Card class="mx-auto w-fit p-0">
    <twig:Card:Content class="p-0">
        <twig:Calendar
            mode="range"
            today="2026-03-15"
            month="2026-01-01"
            :selected="['2026-01-12', '2026-02-11']"
            numberOfMonths="2"
            minDate="1900-01-01"
            maxDate="2026-03-15"
        />
    </twig:Card:Content>
</twig:Card>
```

### Multiple

Use `mode="multiple"` to select any number of individual dates.

```twig
<twig:Card class="mx-auto w-fit p-0">
    <twig:Card:Content class="p-0">
        <twig:Calendar
            mode="multiple"
            today="2026-03-15"
            month="2026-03-01"
            :selected="['2026-03-04', '2026-03-11', '2026-03-18']"
        />
    </twig:Card:Content>
</twig:Card>
```

### Month and Year Selector

Use `captionLayout="dropdown"` to replace the caption with month and year dropdowns. Their range is bounded by `startMonth` and `endMonth`.

```twig
<twig:Calendar
    mode="single"
    today="2026-03-15"
    month="2026-03-01"
    captionLayout="dropdown"
    startMonth="2024-01-01"
    endMonth="2028-12-01"
    class="mx-auto rounded-lg border"
/>
```

### Presets

Anything placed inside the calendar is rendered below the grid, and is within reach of the `calendar` controller: a button carrying `data-action="click->calendar#selectDate"` and a `data-calendar-date-param` selects that date and jumps to its month.

```twig
<twig:Card size="sm" class="mx-auto w-fit max-w-[300px]">
    <twig:Card:Content>
        <twig:Calendar
            mode="single"
            today="2026-03-15"
            selected="2026-02-12"
            fixedWeeks
            class="p-0 [--cell-size:--spacing(9.5)]"
        >
            <div class="mt-3 flex flex-wrap gap-2 border-t pt-3">
                {% for preset in [{label: 'Today', days: 0}, {label: 'Tomorrow', days: 1}, {label: 'In 3 days', days: 3}, {label: 'In a week', days: 7}, {label: 'In 2 weeks', days: 14}] %}
                    <twig:Button
                        variant="outline"
                        size="sm"
                        class="flex-1"
                        data-action="click->calendar#selectDate"
                        data-calendar-date-param="{{ '2026-03-15'|date_modify('+' ~ preset.days ~ ' days')|date('Y-m-d') }}"
                    >{{ preset.label }}</twig:Button>
                {% endfor %}
            </div>
        </twig:Calendar>
    </twig:Card:Content>
</twig:Card>
```

### Date and Time Picker

```twig
<twig:Card size="sm" class="mx-auto w-fit">
    <twig:Card:Content>
        <twig:Calendar
            mode="single"
            today="2026-03-15"
            selected="2026-03-12"
            class="p-0"
        />
    </twig:Card:Content>
    <twig:Card:Footer class="border-t bg-card">
        <twig:Field:Group>
            <twig:Field>
                <twig:Field:Label for="time-from">Start Time</twig:Field:Label>
                <twig:InputGroup>
                    <twig:InputGroup:Input id="time-from" type="time" step="1" value="10:30:00" class="appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none" />
                    <twig:InputGroup:Addon>
                        <twig:ux:icon name="lucide:clock" class="text-muted-foreground" />
                    </twig:InputGroup:Addon>
                </twig:InputGroup>
            </twig:Field>
            <twig:Field>
                <twig:Field:Label for="time-to">End Time</twig:Field:Label>
                <twig:InputGroup>
                    <twig:InputGroup:Input id="time-to" type="time" step="1" value="12:30:00" class="appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none" />
                    <twig:InputGroup:Addon>
                        <twig:ux:icon name="lucide:clock" class="text-muted-foreground" />
                    </twig:InputGroup:Addon>
                </twig:InputGroup>
            </twig:Field>
        </twig:Field:Group>
    </twig:Card:Footer>
</twig:Card>
```

### Booked Dates

Every entry of `modifiers` is rendered as a `data-<name>` attribute on the matching days, which is enough to style them with an arbitrary utility.

```twig
{% set bookedDates = (0..14)|map(offset => '2026-02-12'|date_modify('+' ~ offset ~ ' days')|date('Y-m-d')) %}
<twig:Card class="mx-auto w-fit p-0">
    <twig:Card:Content class="p-0">
        <twig:Calendar
            mode="single"
            today="2026-03-15"
            month="2026-02-01"
            selected="2026-02-03"
            :disabled="bookedDates"
            :modifiers="{booked: bookedDates}"
            class="[&_[data-booked=true][data-disabled=true]]:opacity-100 [&_[data-booked=true]>button]:line-through"
        />
    </twig:Card:Content>
</twig:Card>
```

### Custom Cell Size

Cells are sized with the `--cell-size` CSS variable, which can be overridden per breakpoint.

```twig
<twig:Card class="mx-auto w-fit p-0">
    <twig:Card:Content class="p-0">
        <twig:Calendar
            mode="range"
            today="2026-03-15"
            month="2026-12-01"
            :selected="['2026-12-08', '2026-12-18']"
            captionLayout="dropdown"
            class="[--cell-size:--spacing(10)] md:[--cell-size:--spacing(12)]"
        />
    </twig:Card:Content>
</twig:Card>
```

### Week Numbers

Use `showWeekNumber` to prepend a column with the ISO week number.

```twig
<twig:Card class="mx-auto w-fit p-0">
    <twig:Card:Content class="p-0">
        <twig:Calendar
            mode="single"
            today="2026-03-15"
            month="2026-02-01"
            selected="2026-02-03"
            showWeekNumber
        />
    </twig:Card:Content>
</twig:Card>
```

### API

The calendar dispatches a `calendar:select` event on every selection change, with `selected` (the dates, as `Y-m-d` strings) and `mode` in its detail, and a `calendar:month-change` event on every navigation, with `month` in its detail. Both events bubble. The `calendar-display` controller shipped with this recipe listens to `calendar:select` and mirrors the selection into a visible read-only field.

```twig
<div class="mx-auto flex w-fit flex-col gap-4" data-controller="calendar-display" data-action="calendar:select->calendar-display#update">
    <twig:Calendar
        mode="range"
        name="stay"
        today="2026-03-15"
        :selected="['2026-03-12', '2026-03-18']"
        class="rounded-lg border"
    />
    <twig:Field>
        <twig:Field:Label for="calendar-api-summary">Selected dates</twig:Field:Label>
        <twig:Input id="calendar-api-summary" data-calendar-display-target="output" value="2026-03-12, 2026-03-18" readonly />
    </twig:Field>
</div>
```

### RTL

To enable RTL support, set the `dir="rtl"` attribute on the root element. Pair it with `locale` so the month, weekday and day labels are formatted for that language, and with `weekStartsOn` so the week starts on the expected day.

The `-u-nu-` Unicode extension pins the numbering system the digits are drawn from. Worth setting explicitly for languages that have more than one in use: a bare `ar` resolves to Arabic-Indic digits or Latin ones depending on the ICU version, and the server and the browser do not necessarily ship the same one.

```twig
<div class="flex flex-col items-center gap-12">
    {# Arabic #}
    <twig:Calendar
        dir="rtl"
        locale="ar-u-nu-latn"
        weekStartsOn="6"
        mode="single"
        today="2026-03-15"
        selected="2026-03-15"
        captionLayout="dropdown"
        class="rounded-lg border [--cell-size:--spacing(9)]"
    />

    {# Hebrew #}
    <twig:Calendar
        dir="rtl"
        locale="he-u-nu-latn"
        weekStartsOn="0"
        mode="single"
        today="2026-03-15"
        selected="2026-03-15"
        captionLayout="dropdown"
        class="rounded-lg border [--cell-size:--spacing(9)]"
    />
</div>
```

## Accessibility

- The root element renders `role="group"` with an `aria-label` taken from the `label` prop (`Calendar` by default), and each displayed month is a `<table role="grid">` whose `aria-label` is the localized month and year.
- Weekday headers are `<th scope="col">` showing a narrow abbreviation, with the full weekday name exposed through `aria-label`. Every day cell carries `aria-selected`, and its button has an `aria-label` holding the full localized date, so a screen reader announces the whole date rather than a bare number.
- Only one day button per calendar is in the tab order (roving `tabindex`): the selected day, today, or the first of the displayed month. From there, arrow keys move by one day and one week, `Home` and `End` jump to the edges of the week, `PageUp` and `PageDown` move by one month, and left and right are swapped in RTL. Moving outside the displayed month navigates to it.
- The previous and next month buttons are icon-only with a visually hidden label, hardcoded in the template, so translate them there. They use `aria-disabled` rather than `disabled` at the navigation bounds, so they stay reachable by keyboard and simply do nothing.
- With `captionLayout="dropdown"`, the month and year `<select>` elements are transparent overlays over the visible caption, each named by an `aria-label` (`Month` and `Year`).
- With `showWeekNumber`, the week-number column header is a visually hidden `Week`.
- Month, weekday and day labels are localized through the `locale` prop, falling back to the request locale, so the announced dates follow the user's language.

## API Reference

### `<twig:Calendar>`

| Prop | Type | Default | Description |
|:-----|:-----|:--------|:------------|
| `mode` | `'single'\|'multiple'\|'range'` | `'single'` | The selection mode. |
| `name` | `string\|null` | `null` | The name of the hidden inputs holding the selection, so the calendar can be submitted with a form. |
| `selected` | `string\|array<string>\|null` | `null` | The initially selected dates, as `Y-m-d` strings. In `range` mode, the first entry is the start and the second one the end. |
| `month` | `string\|null` | `null` | The month to display, as a `Y-m-d` string. |
| `numberOfMonths` | `int` | `1` | The number of months displayed side by side. |
| `captionLayout` | `'label'\|'dropdown'` | `'label'` | Whether the caption shows a plain label or month and year dropdowns. |
| `showOutsideDays` | `boolean` | `true` | Whether the days of the surrounding months are displayed. |
| `showWeekNumber` | `boolean` | `false` | Whether a leading column shows the ISO week number. |
| `fixedWeeks` | `boolean` | `false` | Whether every month always displays six weeks. |
| `weekStartsOn` | `int` | `0` | The first day of the week, from `0` for Sunday to `6` for Saturday. |
| `locale` | `string\|null` | `null` | The locale used to format the month, weekday and day labels, falling back to the current request locale. |
| `today` | `string\|null` | `null` | The date considered as today, as a `Y-m-d` string. |
| `disabled` | `array<string>` | `[]` | The dates that cannot be selected, as `Y-m-d` strings. |
| `minDate` | `string\|null` | `null` | The earliest selectable date, as a `Y-m-d` string. |
| `maxDate` | `string\|null` | `null` | The latest selectable date, as a `Y-m-d` string. |
| `startMonth` | `string\|null` | `null` | The earliest month reachable through navigation, as a `Y-m-d` string. |
| `endMonth` | `string\|null` | `null` | The latest month reachable through navigation, as a `Y-m-d` string. |
| `modifiers` | `array<string,array<string>>` | `[]` | Extra flags keyed by name, each holding a list of `Y-m-d` strings, rendered as `data-<name>` on the matching days. |
| `buttonVariant` | `'default'\|'secondary'\|'destructive'\|'outline'\|'ghost'\|'link'` | `'ghost'` | The style variant of the navigation buttons. |
| `label` | `string` | `'Calendar'` | The accessible name of the calendar. |

| Block | Description |
|:------|:------------|
| `content` | Optional content rendered below the month grids, such as a footer. |
### `data-controller="calendar"`

| Value | Type | Default | Description |
|:------|:-----|:--------|:------------|
| `data-calendar-mode-value` | `String` | `'single'` | The selection mode, one of `single`, `multiple` or `range`. |
| `data-calendar-locale-value` | `String` | - | The locale used to format the caption and day labels, falling back to the document language. |
| `data-calendar-name-value` | `String` | - | The name of the hidden inputs mirroring the selection. |
| `data-calendar-month-value` | `String` | - | The first displayed month, as a `Y-m-d` string. |
| `data-calendar-selected-value` | `Array` | - | The selected dates, as `Y-m-d` strings. |
| `data-calendar-disabled-value` | `Array` | - | The dates that cannot be selected, as `Y-m-d` strings. |
| `data-calendar-modifiers-value` | `Object` | - | Extra flags keyed by name, each holding a list of `Y-m-d` strings. |
| `data-calendar-today-value` | `String` | - | The date considered as today, as a `Y-m-d` string. |
| `data-calendar-min-date-value` | `String` | - | The earliest selectable date, as a `Y-m-d` string. |
| `data-calendar-max-date-value` | `String` | - | The latest selectable date, as a `Y-m-d` string. |
| `data-calendar-start-month-value` | `String` | - | The earliest month reachable through navigation, as a `Y-m-d` string. |
| `data-calendar-end-month-value` | `String` | - | The latest month reachable through navigation, as a `Y-m-d` string. |
| `data-calendar-week-starts-on-value` | `Number` | - | The first day of the week, from `0` for Sunday to `6` for Saturday. |
| `data-calendar-number-of-months-value` | `Number` | `1` | The number of months displayed side by side. |
| `data-calendar-show-outside-days-value` | `Boolean` | `true` | Whether the days of the surrounding months are displayed. |
| `data-calendar-show-week-number-value` | `Boolean` | - | Whether the leading week number column is displayed. |
| `data-calendar-fixed-weeks-value` | `Boolean` | - | Whether every month always displays six weeks. |

| Target | Description |
|:-------|:------------|
| `month` | A displayed month, holding its caption and its grid. |
| `previous` | The button moving the calendar to the previous month. |
| `next` | The button moving the calendar to the next month. |
| `input` | A hidden input mirroring the selection. |

| Action | Description |
|:-------|:------------|
| `previousMonth` | Moves the calendar one month backwards. |
| `nextMonth` | Moves the calendar one month forwards. |
| `goToMonth` | Moves the calendar to the month and year picked in the dropdowns. |
| `selectDate` | Selects the date carried by the `date` param, or by the clicked day. |
| `handleKeydown` | Moves the focus between days with the arrow, home, end and page keys. |
| `trackFocus` | Reflects the focused day on its cell. |
| `previewRange` | Previews the range being drawn up to the hovered day. |
| `clearPreview` | Drops the range preview. |
### `data-controller="calendar-display"`

| Target | Description |
|:-------|:------------|
| `output` | The form control reflecting the calendar selection. |

| Action | Description |
|:-------|:------------|
| `update` | Writes the calendar selection into the output target. |