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.
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:
<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.
- Constraints —
min,max,isDateDisabled,disabled,readOnly. - Range behavior —
rangeMode,preventRangeReversal, the hover preview andpreviewRangeoverride. - Environment —
temporal,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).
function ClearButton() {
const { setRange, readOnly } = useCalendarStable();
const { rangeStart, rangeEnd } = useCalendarState();
if (!rangeStart || readOnly) return null;
// …
}
API reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| format | F | "PlainDate" | The value format used for date serialization. Determines the type of
value, defaultValue, min, max, and callback parameters. |
| min | RawValueForFormat<F> | — | Earliest selectable date. Dates before this are disabled. |
| max | RawValueForFormat<F> | — | Latest selectable date. Dates after this are disabled. |
| disabled | boolean | false | When true, the entire calendar is disabled. |
| readOnly | boolean | false | When true, the calendar is read-only. Keyboard navigation still works
but selection is prevented. |
| isDateDisabled | (date: PlainDate) => boolean | — | Callback to disable individual dates. Return true to disable a date.
Called in addition to min/max bounds checking. |
| timeZone | string | The system's current time zone. | IANA time zone identifier used for date/time conversions. |
| locale | string | "en-US" | BCP 47 locale string used for formatting month names, weekday labels, and other locale-sensitive output. |
| temporal | TemporalNamespace | — | The 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. |
| weekStartDay | WeekStartDay | 0 | Day of the week the calendar grid starts on.
0 = Sunday, 1 = Monday, ..., 6 = Saturday. |
| children | React.ReactNode | — | React children. |
| selectionMode | "single" | "range" | "multiple" | "single" | |
| value | RawValueForFormat<F> | DateRange<F> | RawValueForFormat<F>[] | null | — | The controlled selected date. Pass null to clear. |
| defaultValue | RawValueForFormat<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
| Prop | Type | Default | Description |
|---|---|---|---|
| onSelect* | (date: PlainDate) => void | — | Selects (or toggles) a date, respecting the current selection mode. |
| setRange* | (start: PlainDate, end: PlainDate) => void | — | Programmatically sets the range boundaries (normalized so start <= end). |
| selectionMode* | "single" | "range" | "multiple" | — | The active selection mode. |
| disabled* | boolean | — | Whether the entire calendar is disabled. |
| readOnly* | boolean | — | Whether the calendar is read-only. |
| isDateDisabled | (date: PlainDate) => boolean | — | User-supplied predicate for individually disabled dates. |
| minValue | PlainDate | — | Earliest selectable date (resolved from the min prop). |
| maxValue | PlainDate | — | Latest selectable date (resolved from the max prop). |
| timeZone* | string | — | Resolved IANA time zone. |
| locale* | string | — | Resolved BCP 47 locale. |
| temporal* | TemporalNamespace | — | Resolved Temporal namespace. |
| weekStartDay* | WeekStartDay | — | Day the calendar week starts on. |
| rangeMode* | RangeMode | — | Active range selection mode. |
| preventRangeReversal* | boolean | — | Whether 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) => void | — | Sets the hovered date for range preview. |
State context
| Prop | Type | Default | Description |
|---|---|---|---|
| selected* | DateValueObject | undefined | — | The currently selected value as a tagged DateValueObject. |
| selectedDates* | PlainDate[] | — | Flat array of all selected dates (plain dates, sorted). |
| rangeStart* | PlainDate | undefined | — | Start of the current range selection, or undefined. |
| rangeEnd* | PlainDate | undefined | — | End of the current range selection, or undefined. |
| hoveredDate* | PlainDate | undefined | — | The currently hovered date (for range preview). |
| previewStart* | PlainDate | undefined | — | Start of the computed preview range. |
| previewEnd* | PlainDate | undefined | — | End of the computed preview range. |
| baseRootState* | BaseRootState | — | View-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. |