Components

CalendarProvider

Manages shared state across calendar views — selection, bounds, locale, and more.

CalendarProvider is the state root of every calendar. It owns the selection (in all three modes), resolves configuration — bounds, time zone, locale, week start, the Temporal implementation — and shares everything with its descendants through context. It renders no DOM of its own.

tsx
import { Temporal } from "@js-temporal/polyfill";
import { CalendarProvider, MonthView } from "@klinking/colander";

<CalendarProvider
  temporal={Temporal}
  selectionMode="range"
  weekStartDay={1}
  onValueChange={(range) => console.log(range)}
>
  <MonthView.Root>{/* navigation + grid */}</MonthView.Root>
</CalendarProvider>;

When you need it explicitly

The MonthView and WeeksView wrappers already include a CalendarProvider, so most calendars never mention it. Reach for the explicit form when:

  • Components outside the view need calendar state. Anything using useCalendarStable() / useCalendarState() must live under the provider — a selection summary, preset-range buttons, a clear button.
  • Two views should share one selection. Render both roots under a single provider — for example a month grid next to a scrolling weeks strip, always in sync:
tsx
<CalendarProvider temporal={Temporal} selectionMode="range">
  <MonthView.Root>{/* … */}</MonthView.Root>
  <WeeksView.Root weekCount={3}>{/* … */}</WeeksView.Root>
</CalendarProvider>
  • You're building a custom view from the grid primitives and hooks.

Don't nest a MonthView/WeeksView wrapper inside a CalendarProvider — the wrapper creates its own provider, which would shadow yours. Inside an explicit provider, always use MonthView.Root / WeeksView.Root.

What it manages

  • Selection — single / range / multiple, controlled or uncontrolled, exposed in your configured value format.
  • Constraintsmin, max, isDateDisabled, disabled, readOnly.
  • Range behaviorrangeMode, preventRangeReversal, the hover preview and previewRange override.
  • Environmenttemporal, timeZone, locale, weekStartDay.

What it does not manage: focus, keyboard navigation, and which month/weeks are visible — those belong to the view roots (MonthView, WeeksView), which is why the provider alone renders nothing interactive.

Reading state with hooks

Two hooks expose the provider's context, split by volatility so components can subscribe to only what they need:

  • useCalendarStable() — configuration and stable callbacks (onSelect, setRange, selectionMode, minValue, maxValue, temporal, locale, timeZone, …). Doesn't change during normal interaction.
  • useCalendarState() — the live values (selected, selectedDates, rangeStart, rangeEnd, hoveredDate, previewStart, previewEnd).
tsx
function ClearButton() {
  const { setRange, readOnly } = useCalendarStable();
  const { rangeStart, rangeEnd } = useCalendarState();
  if (!rangeStart || readOnly) return null;
  // …
}

API reference

Props

PropTypeDefaultDescription
formatF"PlainDate"The value format used for date serialization. Determines the type of value, defaultValue, min, max, and callback parameters.
minRawValueForFormat<F>Earliest selectable date. Dates before this are disabled.
maxRawValueForFormat<F>Latest selectable date. Dates after this are disabled.
disabledbooleanfalseWhen true, the entire calendar is disabled.
readOnlybooleanfalseWhen true, the calendar is read-only. Keyboard navigation still works but selection is prevented.
isDateDisabled(date: PlainDate) => booleanCallback to disable individual dates. Return true to disable a date. Called in addition to min/max bounds checking.
timeZonestringThe system's current time zone.IANA time zone identifier used for date/time conversions.
localestring"en-US"BCP 47 locale string used for formatting month names, weekday labels, and other locale-sensitive output.
temporalTemporalNamespaceThe Temporal implementation to use. Not bundled — provide a Temporal namespace (e.g. from temporal-polyfill or @js-temporal/polyfill) unless the host exposes native Temporal. See RootOwnProps.temporal for links.
weekStartDayWeekStartDay0Day of the week the calendar grid starts on. 0 = Sunday, 1 = Monday, ..., 6 = Saturday.
childrenReact.ReactNodeReact children.
selectionMode"single" | "range" | "multiple""single"
valueRawValueForFormat<F> | DateRange<F> | RawValueForFormat<F>[] | nullThe controlled selected date. Pass null to clear.
defaultValueRawValueForFormat<F> | DateRange<F> | RawValueForFormat<F>[]
onValueChange((value: RawValueForFormat<F> | null, meta: ValueChangeMeta<RawValueForFormat<F> | null>) => void) | ((value: RawValueForFormat<F> | null, meta: ValueChangeMeta<RawValueForFormat<F> | null>) => void) | ((value: DateRange<F> | null, meta: ValueChangeMeta<DateRange<F> | null>) => void) | ((value: DateRange<F> | null, meta: ValueChangeMeta<DateRange<F> | null>) => void) | ((value: RawValueForFormat<F>[], meta: ValueChangeMeta<RawValueForFormat<F>[]>) => void) | ((value: RawValueForFormat<F>[], meta: ValueChangeMeta<RawValueForFormat<F>[]>) => void)

Stable context

PropTypeDefaultDescription
onSelect*(date: PlainDate) => voidSelects (or toggles) a date, respecting the current selection mode.
setRange*(start: PlainDate, end: PlainDate) => voidProgrammatically sets the range boundaries (normalized so start <= end).
selectionMode*"single" | "range" | "multiple"The active selection mode.
disabled*booleanWhether the entire calendar is disabled.
readOnly*booleanWhether the calendar is read-only.
isDateDisabled(date: PlainDate) => booleanUser-supplied predicate for individually disabled dates.
minValuePlainDateEarliest selectable date (resolved from the min prop).
maxValuePlainDateLatest selectable date (resolved from the max prop).
timeZone*stringResolved IANA time zone.
locale*stringResolved BCP 47 locale.
temporal*TemporalNamespaceResolved Temporal namespace.
weekStartDay*WeekStartDayDay the calendar week starts on.
rangeMode*RangeModeActive range selection mode.
preventRangeReversal*booleanWhether reversed ranges are auto-sorted instead of collapsed.
valueFormat*"object" | "PlainDate" | "PlainDateTime" | "PlainMonthDay" | "PlainTime" | "PlainYearMonth" | "ZonedDateTime" | "Date"The value format discriminant.
setHoveredDate*(date: PlainDate | undefined) => voidSets the hovered date for range preview.

State context

PropTypeDefaultDescription
selected*DateValueObject | undefinedThe currently selected value as a tagged DateValueObject.
selectedDates*PlainDate[]Flat array of all selected dates (plain dates, sorted).
rangeStart*PlainDate | undefinedStart of the current range selection, or undefined.
rangeEnd*PlainDate | undefinedEnd of the current range selection, or undefined.
hoveredDate*PlainDate | undefinedThe currently hovered date (for range preview).
previewStart*PlainDate | undefinedStart of the computed preview range.
previewEnd*PlainDate | undefinedEnd of the computed preview range.
baseRootState*BaseRootStateView-independent part of the render-prop RootState (selection + resolved config), built once here so MonthView and WeeksView expose identical state. Each view adds its own focused/viewing.