---
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.
category: Data display
---
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
Calendars let users pick one or more dates from a month grid. They support single, multiple, and range selection without leaving the surrounding form.
## Try it
## 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';
```
## Derived tree
`` 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. Parent hosts (`rui-date-field`, `rui-date-range-picker`) embed `` and query `[data-calendar-day]` inside it when opening a popup.
## Selection modes
`single` selects one date. `multiple` accepts comma-separated values. `range` expects `start/end` in `value`.
## Constrain selectable dates
Set `min` and `max` to block out-of-range bookings or past dates. Pair with `visibleMonths` for multi-month range pickers.
## 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. Override at the theme layer (`tokens/presets/colors/*.css`, `tokens/radius/*.css`), not in component CSS.
## API
`RuiCalendar` is a custom element (``) that 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.