Calendar
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.
The <Calendar> component is an inline date picker. It supports single, range,
and multi-select modes; click the title to drill down through month and year
views; and respects the active locale's first day of week, weekend days, and
weekday names.
Basic usage
Selection modes
Calendar's selection shape is controlled by the mode prop. The
value / defaultValue / onChange types are inferred from mode:
mode | value type |
|---|---|
'single' (default) | CalendarDate | null |
'range' | { start: CalendarDate; end: CalendarDate | null } | null |
'multiple' | CalendarDate[] |
Range selection
Two-month layout on desktop (visibleMonths={2}), single-month on mobile by default.
Use minNights / maxNights to constrain the gap between start and end —
useful for hotel bookings (minimum stay) or short-term rental floors / caps.
Multiple selection
Visible months
visibleMonths controls how many month grids render side-by-side. Pass an
explicit number, or 'auto' (default for range mode) to render two on
viewports ≥ 768px and one on smaller screens.
Drill-down navigation
Click the title to toggle between day, month, and year views.
defaultView="year" opens directly into the decade picker — handy for
birthday inputs.
The view is controllable via view + onViewChange:
Header layout: caption
The header has three layouts:
caption="title"(default) — centered title, click to drill down.caption="dropdown"—[ May ▾ ] [ 2026 ▾ ]Select pills.caption="custom"— render your own header via<Calendar.Caption>.
Dropdown captions
Replaces the title-click drilldown with two Select pills — one for
the month, one for the year. Useful when the user is likely to jump
across years (birthdays, far-future bookings).
yearRange defaults derive from minValue / maxValue if set, else
[focused.year - 100, focused.year + 10] — covers birthday-pickers
and short-term-booking ranges.
Custom captions (slot composition)
Switch to caption="custom" to drop the built-in caption entirely
and render your own dropdowns or nav. Wrap them in <Calendar.Caption>
to make the slot intent explicit:
Inside any descendant, call useCalendarCaption() for state + setters:
The full hook return shape:
| Field | Type | Notes |
|---|---|---|
month | number | 1..12, currently visible month |
year | number | Currently visible year |
visibleMonth | CalendarDate | First day of the visible month |
monthOptions | ReadonlyArray<CaptionOption> | All months; disabled set if outside min/max |
yearOptions | ReadonlyArray<CaptionOption> | Years inside yearRange |
setMonth | (month: number) => void | Snap to a specific month (1..12) |
setYear | (year: number) => void | Snap to a specific year |
goPrev | () => void | Page back (month/year/decade per current view) |
goNext | () => void | Page forward (month/year/decade per current view) |
Constraining selection
Use minValue / maxValue to bound the selectable range, and
isDateUnavailable for arbitrary disables (weekends, holidays,
server-fetched blackout periods). Disabled cells render dimmed and
cannot be focused or selected.
Min / max
Disabled dates
isDateUnavailable is a synchronous predicate the Calendar calls for
every visible cell. Combine it freely with bounds.
Custom day rendering
Use the renderDay slot to overlay availability dots, prices, or
per-day decorations. The renderer receives a DayContext:
| Field | Type | When true |
|---|---|---|
date | CalendarDate | The cell's date |
isOutsideMonth | boolean | Cell belongs to the previous / next month |
isToday | boolean | Matches today() in the local timezone |
isSelected | boolean | Selected (single / multi) or range endpoint |
isRangeStart | boolean | Range-mode start |
isRangeEnd | boolean | Range-mode end |
isInRange | boolean | Strictly between start and end |
isInPreviewRange | boolean | Hover preview between start and pointer |
isUnavailable | boolean | Outside min/max or isDateUnavailable true |
isFocused | boolean | Has keyboard focus |
isWeekend | boolean | Locale-derived weekend day |
Display options
A handful of small toggles that don't fit elsewhere:
showWeekNumbers adds a left column rendering ISO 8601 week numbers
for each visible row — common in scheduling UIs and European calendars.
highlightToday paints today's cell with the accent color; turn it
off in calendars where "today" isn't a meaningful anchor (e.g. browsing
historical dates).
Internationalization
Calendar reads its locale from NoriProvider.locale, falling back to
new Intl.DateTimeFormat().resolvedOptions().locale. You can also
override per-component:
The active locale drives weekday names, weekday order, weekend marking,
and (via Select's locale-aware sort) the dropdown captions.
Overriding locale defaults
firstDayOfWeek and weekendDays are derived from the locale via
CLDR; both are overridable when you need to match a specific
business calendar:
firstDayOfWeek and weekendDays are 0..6 where 0 = Sunday.
onChange metadata
Every change fires onChange(value, meta) where meta describes how the
change happened:
Use it to decide whether to dismiss a popover (source === 'click')
versus keep it open during keyboard navigation, or to know if a year-view
selection completed a date pick.
Scroll behavior
By default <Calendar> renders one month at a time (paged) with prev/next
chevrons in the header. Set behavior="scroll" to render a vertically
scrollable list of month panels instead — handy on touch surfaces and for
date pickers where the user is browsing rather than navigating.
When behavior="scroll" is active:
-
The header chevrons advance the focused month one panel at a time — the list scrolls to bring the new month into view rather than swapping the panel in place.
-
The dropdown caption (
caption="dropdown") also scrolls the list to the chosen month / year instead of paging. -
visibleMonthsis ignored — scroll mode is single-column by definition. PassingvisibleMonths > 1together withbehavior="scroll"logs a development-mode warning and falls back to single column. -
On native, scroll mode requires the optional peer dependency
@marceloterreiro/flash-calendar:Web doesn't require any additional dependency — the scroll container is a plain
<div>and focused-month tracking usesIntersectionObserver.
Keyboard
| Key | Action |
|---|---|
← / → | Move focus by 1 day |
↑ / ↓ | Move focus by 1 week |
PgUp / PgDn | Move focus by 1 month |
Shift + PgUp/Dn | Move focus by 1 year |
Home / End | Move to start/end of week |
Enter / Space | Select focused date |
Disabled cells are skipped during keyboard navigation. If every cell in the direction of motion is disabled, focus stays put.
Props
<Calendar>
| Prop | Type | Default | Description |
|---|---|---|---|
behavior | enum | — | Default 'paged' on web, 'scroll' on native (Phase 2). |
caption | enum | — | Header layout. `'title'` (default) shows a centered "May 2026 ▾" drilldown button. `'dropdown'` replaces it with `[ May ▾ ] [ 2026 ▾ ]` pickers powered by `Select`. `'custom'` hides the built-in caption and renders the Calendar's children — typically wrapped in `<Calendar.Caption>` — driving them via `useCalendarCaption()`. |
className | string | — | — |
defaultValue | CalendarDate | CalendarDate[] | DateRange | null | — | — |
defaultView | enum | — | — |
firstDayOfWeek | enum | — | Override locale firstDayOfWeek (0=Sun..6=Sat). |
highlightToday | boolean | — | Highlight today's cell. @defaultValue true |
isDateUnavailable | (date: CalendarDate) => boolean | — | Predicate marking a date unavailable (cannot be focused or selected). |
locale | string | — | Override `NoriProvider.locale`. |
maxNights | number | — | Range mode only — maximum nights between start and end. |
maxValue | CalendarDate | — | Inclusive maximum selectable date. |
minNights | number | — | Range mode only — minimum nights between start and end. |
minValue | CalendarDate | — | Inclusive minimum selectable date. |
mode | enum | — | — |
onChange | (value: CalendarValue<M>, meta: ChangeMeta) => void | — | — |
onViewChange | (view: CalendarView) => void | — | — |
ref | Ref<View> | — | — |
renderDay | (ctx: DayContext) => ReactNode | — | Custom renderer for a single day cell. |
showWeekNumbers | boolean | — | Render the ISO week number column. |
testID | string | — | Test id for the root element. |
value | CalendarDate | CalendarDate[] | DateRange | null | — | — |
view | enum | — | — |
visibleMonths | number | "auto" | — | Number of calendar months side-by-side. `'auto'` = 2 on ≥768px, else 1. |
weekendDays | DayOfWeek[] | — | Override locale weekend marking. |
yearRange | [min: number, max: number] | — | Year-dropdown bounds, inclusive. Only honored when `caption !== 'title'`. Defaults derive from `minValue` / `maxValue` if set, else `[focused.year - 100, focused.year + 10]` — covers the common birthday-picker and short-term-booking ranges. |
<Calendar.Caption>
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Custom caption content — typically your own month / year selects. |