{
  "title": "Calendar",
  "description": "Inline visual calendar for picking dates, ranges, or multiple selections — with locale-aware first day of week, weekend marking, drill-down navigation, and slot-composable headers.",
  "url": "/docs/components/calendar",
  "since": "1.0.0",
  "tags": [
    "date",
    "picker",
    "range",
    "i18n",
    "a11y"
  ],
  "platform": "both",
  "source": "---\ntitle: Calendar\ndescription: Inline visual calendar for picking dates, ranges, or multiple selections — with locale-aware first day of week, weekend marking, drill-down navigation, and slot-composable headers.\nsince: 1.0.0\ntags: [date, picker, range, i18n, a11y]\nplatform: both\ncategory: inputs\n---\n\nimport { BundleSize } from '@/components/bundle-size';\nimport { Preview } from '@/components/preview';\nimport { PropsTable } from '@/components/props-table';\n\n<BundleSize component=\"Calendar\" />\n\nThe `<Calendar>` component is an inline date picker. It supports single, range,\nand multi-select modes; click the title to drill down through month and year\nviews; and respects the active locale's first day of week, weekend days, and\nweekday names.\n\n## Basic usage\n\n<Preview name=\"calendar-basic\" />\n\n```tsx\nimport { useState } from 'react';\nimport { Calendar } from '@nori-ui/core';\nimport type { CalendarDate } from '@internationalized/date';\n\nconst [value, setValue] = useState<CalendarDate | null>(null);\n\n<Calendar value={value} onChange={(v) => setValue(v)} />\n```\n\n## Selection modes\n\nCalendar's selection shape is controlled by the `mode` prop. The\n`value` / `defaultValue` / `onChange` types are inferred from `mode`:\n\n| `mode`       | `value` type                          |\n|--------------|---------------------------------------|\n| `'single'` (default) | `CalendarDate \\| null`        |\n| `'range'`    | `{ start: CalendarDate; end: CalendarDate \\| null } \\| null` |\n| `'multiple'` | `CalendarDate[]`                      |\n\n### Range selection\n\nTwo-month layout on desktop (`visibleMonths={2}`), single-month on mobile by default.\n\n<Preview name=\"calendar-range\" />\n\nUse `minNights` / `maxNights` to constrain the gap between start and end —\nuseful for hotel bookings (minimum stay) or short-term rental floors / caps.\n\n```tsx\n<Calendar mode=\"range\" minNights={2} maxNights={14} />\n```\n\n### Multiple selection\n\n<Preview name=\"calendar-multiple\" />\n\n## Visible months\n\n`visibleMonths` controls how many month grids render side-by-side. Pass an\nexplicit number, or `'auto'` (default for range mode) to render two on\nviewports ≥ 768px and one on smaller screens.\n\n```tsx\n<Calendar visibleMonths={3} />          // always three months\n<Calendar mode=\"range\" visibleMonths=\"auto\" /> // 2 on desktop, 1 on mobile\n```\n\n## Drill-down navigation\n\nClick the title to toggle between day, month, and year views.\n`defaultView=\"year\"` opens directly into the decade picker — handy for\nbirthday inputs.\n\n<Preview name=\"calendar-drilldown\" />\n\nThe view is controllable via `view` + `onViewChange`:\n\n```tsx\nconst [view, setView] = useState<CalendarView>('day');\n<Calendar view={view} onViewChange={setView} />\n```\n\n## Header layout: `caption`\n\nThe header has three layouts:\n\n- `caption=\"title\"` (default) — centered title, click to drill down.\n- `caption=\"dropdown\"` — `[ May ▾ ] [ 2026 ▾ ]` Select pills.\n- `caption=\"custom\"` — render your own header via `<Calendar.Caption>`.\n\n### Dropdown captions\n\nReplaces the title-click drilldown with two `Select` pills — one for\nthe month, one for the year. Useful when the user is likely to jump\nacross years (birthdays, far-future bookings).\n\n<Preview name=\"calendar-dropdown-caption\" />\n\n```tsx\n<Calendar caption=\"dropdown\" yearRange={[2020, 2030]} />\n```\n\n`yearRange` defaults derive from `minValue` / `maxValue` if set, else\n`[focused.year - 100, focused.year + 10]` — covers birthday-pickers\nand short-term-booking ranges.\n\n### Custom captions (slot composition)\n\nSwitch to `caption=\"custom\"` to drop the built-in caption entirely\nand render your own dropdowns or nav. Wrap them in `<Calendar.Caption>`\nto make the slot intent explicit:\n\n```tsx\n<Calendar caption=\"custom\">\n    <Calendar.Caption>\n        <MyMonthSelect />\n        <MyYearSelect />\n    </Calendar.Caption>\n</Calendar>\n```\n\nInside any descendant, call `useCalendarCaption()` for state + setters:\n\n```tsx\nimport { useCalendarCaption } from '@nori-ui/core';\n\nconst MyMonthSelect = () => {\n    const { month, monthOptions, setMonth } = useCalendarCaption();\n    return <YourSelect value={month} onChange={setMonth} options={monthOptions} />;\n};\n```\n\nThe full hook return shape:\n\n| Field            | Type                                | Notes                                              |\n|------------------|-------------------------------------|----------------------------------------------------|\n| `month`          | `number`                            | 1..12, currently visible month                     |\n| `year`           | `number`                            | Currently visible year                             |\n| `visibleMonth`   | `CalendarDate`                      | First day of the visible month                     |\n| `monthOptions`   | `ReadonlyArray<CaptionOption>`      | All months; `disabled` set if outside min/max      |\n| `yearOptions`    | `ReadonlyArray<CaptionOption>`      | Years inside `yearRange`                           |\n| `setMonth`       | `(month: number) => void`           | Snap to a specific month (1..12)                   |\n| `setYear`        | `(year: number) => void`            | Snap to a specific year                            |\n| `goPrev`         | `() => void`                        | Page back (month/year/decade per current view)     |\n| `goNext`         | `() => void`                        | Page forward (month/year/decade per current view)  |\n\n## Constraining selection\n\nUse `minValue` / `maxValue` to bound the selectable range, and\n`isDateUnavailable` for arbitrary disables (weekends, holidays,\nserver-fetched blackout periods). Disabled cells render dimmed and\ncannot be focused or selected.\n\n### Min / max\n\n<Preview name=\"calendar-min-max\" />\n\n```tsx\nimport { Calendar } from '@nori-ui/core';\nimport { today, getLocalTimeZone } from '@internationalized/date';\n\nconst start = today(getLocalTimeZone());\nconst end = start.add({ days: 30 });\n\n<Calendar minValue={start} maxValue={end} />\n```\n\n### Disabled dates\n\n`isDateUnavailable` is a synchronous predicate the Calendar calls for\nevery visible cell. Combine it freely with bounds.\n\n<Preview name=\"calendar-disabled-dates\" />\n\n```tsx\n<Calendar\n    isDateUnavailable={(date) => {\n        const dow = date.toDate('UTC').getUTCDay();\n        return dow === 0 || dow === 6; // disable weekends\n    }}\n/>\n```\n\n## Custom day rendering\n\nUse the `renderDay` slot to overlay availability dots, prices, or\nper-day decorations. The renderer receives a `DayContext`:\n\n| Field               | Type            | When true                                    |\n|---------------------|-----------------|----------------------------------------------|\n| `date`              | `CalendarDate`  | The cell's date                              |\n| `isOutsideMonth`    | `boolean`       | Cell belongs to the previous / next month    |\n| `isToday`           | `boolean`       | Matches `today()` in the local timezone      |\n| `isSelected`        | `boolean`       | Selected (single / multi) or range endpoint  |\n| `isRangeStart`      | `boolean`       | Range-mode start                             |\n| `isRangeEnd`        | `boolean`       | Range-mode end                               |\n| `isInRange`         | `boolean`       | Strictly between start and end               |\n| `isInPreviewRange`  | `boolean`       | Hover preview between start and pointer      |\n| `isUnavailable`     | `boolean`       | Outside min/max or `isDateUnavailable` true  |\n| `isFocused`         | `boolean`       | Has keyboard focus                           |\n| `isWeekend`         | `boolean`       | Locale-derived weekend day                   |\n\n<Preview name=\"calendar-custom-render\" />\n\n## Display options\n\nA handful of small toggles that don't fit elsewhere:\n\n```tsx\n<Calendar showWeekNumbers />     // adds an ISO week-number column\n<Calendar highlightToday={false} /> // disables today's accent (default: on)\n```\n\n`showWeekNumbers` adds a left column rendering ISO 8601 week numbers\nfor each visible row — common in scheduling UIs and European calendars.\n`highlightToday` paints today's cell with the accent color; turn it\noff in calendars where \"today\" isn't a meaningful anchor (e.g. browsing\nhistorical dates).\n\n## Internationalization\n\nCalendar reads its locale from `NoriProvider.locale`, falling back to\n`new Intl.DateTimeFormat().resolvedOptions().locale`. You can also\noverride per-component:\n\n```tsx\n<Calendar locale=\"de-DE\" />\n```\n\nThe active locale drives weekday names, weekday order, weekend marking,\nand (via `Select`'s locale-aware sort) the dropdown captions.\n\n### Overriding locale defaults\n\n`firstDayOfWeek` and `weekendDays` are derived from the locale via\nCLDR; both are overridable when you need to match a specific\nbusiness calendar:\n\n```tsx\n// Always start the week on Monday, regardless of locale\n<Calendar firstDayOfWeek={1} />\n\n// Treat Friday + Saturday as the weekend (e.g., MENA defaults)\n<Calendar weekendDays={[5, 6]} />\n```\n\n`firstDayOfWeek` and `weekendDays` are `0..6` where `0 = Sunday`.\n\n## `onChange` metadata\n\nEvery change fires `onChange(value, meta)` where `meta` describes how the\nchange happened:\n\n```tsx\ntype ChangeMeta = {\n    view: 'day' | 'month' | 'year';\n    source: 'click' | 'keyboard' | 'scroll';\n};\n```\n\nUse it to decide whether to dismiss a popover (`source === 'click'`)\nversus keep it open during keyboard navigation, or to know if a year-view\nselection completed a date pick.\n\n## Scroll behavior\n\nBy default `<Calendar>` renders one month at a time (paged) with prev/next\nchevrons in the header. Set `behavior=\"scroll\"` to render a vertically\nscrollable list of month panels instead — handy on touch surfaces and for\ndate pickers where the user is browsing rather than navigating.\n\n<Preview name=\"calendar-scroll\" />\n\n```tsx\nimport { Calendar } from '@nori-ui/core';\nimport { getLocalTimeZone, today } from '@internationalized/date';\n\n<Calendar behavior=\"scroll\" defaultValue={today(getLocalTimeZone())} />\n```\n\nWhen `behavior=\"scroll\"` is active:\n\n- The header chevrons advance the focused month one panel at a time — the\n  list scrolls to bring the new month into view rather than swapping the\n  panel in place.\n- The dropdown caption (`caption=\"dropdown\"`) also scrolls the list to the\n  chosen month / year instead of paging.\n- `visibleMonths` is ignored — scroll mode is single-column by definition.\n  Passing `visibleMonths > 1` together with `behavior=\"scroll\"` logs a\n  development-mode warning and falls back to single column.\n- On native, scroll mode requires the optional peer dependency\n  [`@marceloterreiro/flash-calendar`](https://github.com/MarceloPrado/flash-calendar):\n\n  ```bash\n  yarn add @marceloterreiro/flash-calendar\n  ```\n\n  Web doesn't require any additional dependency — the scroll container is\n  a plain `<div>` and focused-month tracking uses `IntersectionObserver`.\n\n## Keyboard\n\n| Key | Action |\n| --- | --- |\n| `←` / `→` | Move focus by 1 day |\n| `↑` / `↓` | Move focus by 1 week |\n| `PgUp` / `PgDn` | Move focus by 1 month |\n| `Shift + PgUp/Dn` | Move focus by 1 year |\n| `Home` / `End` | Move to start/end of week |\n| `Enter` / `Space` | Select focused date |\n\nDisabled cells are skipped during keyboard navigation. If every cell in\nthe direction of motion is disabled, focus stays put.\n\n## Props\n\n### `<Calendar>`\n\n<PropsTable component=\"Calendar\" />\n\n### `<Calendar.Caption>`\n\n<PropsTable component=\"CalendarCaption\" />\n"
}
