---
title: Calendar
description: Calendars let users pick one or more dates from a month grid. They support single, multiple, and range selection without leaving the surrounding form.
---

import { meta as CalendarMeta, Default, Multiple, Range } from '@/content/stories/calendar';
import Canvas from '@/components/component-docs/canvas';
import Demo from '@/components/component-docs/demo';

export const config = {
	dependencies: {
		components: [Canvas, Demo],
		scripts: [
			'../../../components/component-docs/demo.script.tsx',
			'../../../components/component-docs/canvas.script.tsx',
			'../../../components/component-docs/controls.script.tsx',
		],
	},
};


# Calendar

<p class="docs-lede">Calendars let users pick one or more dates from a month grid. They support single, multiple, and range selection without leaving the surrounding form.</p>

## Try it

<Demo of={Default} meta={CalendarMeta} />

## Usage

Bind `value` as an ISO date string. Use `selectionMode` to switch between a single day, multiple days, or a start/end range.

```tsx
import { RuiCalendar } from '@ecopages/radiant-ui/calendar';

<RuiCalendar
  selectionMode="single"
  value="2026-08-07"
  min="2026-01-01"
  max="2026-12-31"
/>
```

## Derived tree

`<rui-calendar>` is a **Derived Tree** host: it `render()`s the month grid from `value`, `min`, `max`, and `selection-mode`. You do not stamp `data-calendar-*` targets yourself, and `RuiCalendar` children are a type error. Parent hosts (`rui-date-field`, `rui-date-range-picker`) embed `<rui-calendar>` and query `[data-calendar-day]` inside it when opening a popup.

## Selection modes

<p>`single` selects one date. `multiple` accepts comma-separated values. `range` expects `start/end` in `value`.</p>

<Canvas of={Multiple} meta={CalendarMeta} />
<Canvas of={Range} meta={CalendarMeta} />

## Constrain selectable dates

<p>Set `min` and `max` to block out-of-range bookings or past dates. Pair with `visibleMonths` for multi-month range pickers.</p>

## Theming

Calendar surfaces map to **semantic surface and selection roles**, never Tailwind palette steps:

| Part | CSS roles |
| --- | --- |
| Month surface (`rui-calendar`) | `border`, `background`, `radius-container`, `space-inset`, `shadow-sm` |
| Month title / weekday labels | `on-background`, `on-surface` |
| Nav + today buttons | `rounded-control`, `surface` hover, `on-surface`, `primary` text |
| Selected day | `primary`, `on-primary` (hover: `primary-container`, `on-primary-container`) |
| Range preview / middle days | `primary` at reduced opacity |
| Outside-month / disabled days | `on-surface` + `opacity-muted` |
| Focus ring | `focus-ring` |

Geometry shares control tokens (`--size-control-*`, `--radius-control`, `--space-control-*`) with `RuiInput` and `RuiButton` so adjacent form rows align. Set semantic values such as `--primary` in your app's theme stylesheet. To choose a shared radius scale, import a public preset such as `@ecopages/radiant-ui/tokens/radius/default`; override `--radius-control` in your app's stylesheet for a custom shape.

## API

`RuiCalendar` is the `<rui-calendar>` custom-element class and `RuiCalendarElement` is an alias for the same class. It renders its own light-DOM month grid(s).

### Attributes

| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | `''` | Selected date(s) in the mode-specific ISO format. |
| `min` | `string` | `''` | Earliest selectable ISO date. |
| `max` | `string` | `''` | Latest selectable ISO date. |
| `disabled` | `boolean` | `false` | Disable all date selection. |
| `locale` | `string` | `''` | BCP 47 locale tag, or comma-separated fallback list. |
| `selection-mode` | `single` · `multiple` · `range` | `single` | Single, multiple, or range selection. |
| `visible-months` | `number` | `1` | Month grids shown side by side. |
| `page-behavior` | `visible` · `single` | `visible` | Whether paging moves by all visible months or one month. |

### Internal targets (generated)

| Target | Host writes | Notes |
| --- | --- | --- |
| `[data-calendar-prev-month]` / `[data-calendar-next-month]` | `disabled` | paging controls |
| `[data-calendar-grid]` | `role="grid"` | month grid |
| `[data-calendar-day]` | `data-iso`, `tabIndex`, `aria-selected`, `disabled` | day buttons |
| `[data-calendar-today]` | `disabled` | jump to today |

Parent hosts query `[data-calendar-day][data-iso="…"]` for popup focus.

### Events

| Event | Detail | Description |
| --- | --- | --- |
| `rui-change` | `{ value, start?, end? }` | Fired when the selection changes; `start` / `end` are set for `range` mode. |

### CSS classes

Public BEM classes on the composed light-DOM surface (documented via `@cssclass` on `RuiCalendar`):

| Class | Description |
| --- | --- |
| `.rui-calendar` | Root surface (`role="application"`). |
| `.rui-calendar--multi` | Multi-month root modifier. |
| `.rui-calendar__months` | Row of month panels. |
| `.rui-calendar__month-panel` | Single month grid column. |
| `.rui-calendar__header` | Month title row with paging controls. |
| `.rui-calendar__nav` | Prev / next month button. |
| `.rui-calendar__nav-spacer` | Placeholder where a nav button is absent. |
| `.rui-calendar__month` | Month title. |
| `.rui-calendar__weekdays` | Weekday header row. |
| `.rui-calendar__weekday` | Weekday label. |
| `.rui-calendar__grid` | Month grid (`role="grid"`). |
| `.rui-calendar__week` | Grid row. |
| `.rui-calendar__day` | Day cell button (`role="gridcell"`). |
| `.rui-calendar__footer` | Footer row with the today button. |
| `.rui-calendar__today` | "Today" jump button. |

### Theme roles

| Part | CSS variables consumed |
| --- | --- |
| Month surface | `--border`, `--background`, `--radius-container`, `--space-inset`, `--shadow-sm` |
| Month title / weekday labels | `--on-background`, `--on-surface` |
| Nav + today buttons | `--radius-control`, `--surface`, `--on-surface`, `--primary` |
| Selected day | `--primary`, `--on-primary`, `--primary-container`, `--on-primary-container` |
| Range preview / middle days | `--primary` |
| Outside / disabled | `--on-surface`, `--opacity-muted` |
| Focus ring | `--focus-ring` |

## Accessibility

- Day cells expose grid semantics with arrow-key navigation between dates.
- Selected and today states are communicated with `aria-selected` and visible styling.
- Provide a visible label or `aria-label` when the calendar is not described by surrounding text.
