Guides
Composition
How Colander components fit together — templates, the render prop, and building your own parts.
Colander is a set of compound components: small parts that you arrange in JSX to form a calendar. This page explains the component tree, the template components that repeat themselves, the render prop, and how to drop down to hooks when you need a part the library doesn't ship.
The component tree
CalendarProvider selection state, bounds, locale, time zone
└─ MonthView.Root │ WeeksView.Root view state: visible month(s) / week window, focus
├─ PrevMonthButton / NextMonthButton (or PrevWeeksButton / NextWeeksButton)
├─ MonthYearString localized label, labels the grid
└─ Grid <table role="grid">
├─ GridHeader <thead>
│ └─ GridHeaderCell <th> weekday labels (×7)
└─ GridBody <tbody>
└─ WeekTemplate <tr> — repeated per visible week
├─ WeekNumberCell optional <td> week number
├─ RangeSelected optional range overlay <td>
├─ RangePreview optional hover-preview overlay <td>
└─ DayCellTemplate <td role="gridcell"> — repeated per day
└─ DayButton <button> — the interactive day
├─ RangeStartDragHandle optional
└─ RangeEndDragHandle optional
Two rules make the tree work:
- State flows down through context.
CalendarProviderowns the selection; the view root owns navigation and focus; grid parts read both. No prop drilling. - You write structure once; templates repeat it.
Templates
WeekTemplate, DayCellTemplate, and GridHeaderCell are templates: the single element you write is instantiated for every week, day, and weekday in view. This keeps a full calendar's JSX to a dozen lines while letting you customize the one repeated unit:
<GridBody>
<WeekTemplate>
<DayCellTemplate>
<DayButton className="day" />
</DayCellTemplate>
</WeekTemplate>
</GridBody>
Each instance receives its own state — DayCellTemplate and DayButton know their date, columnIndex, and every selection flag for that specific day. You can also opt out of iteration: DayCellTemplate date={someDate} renders a single explicit cell, and GridHeaderCell index={0} renders just one weekday header.
Convenience wrappers vs. explicit provider
MonthView and WeeksView fold a CalendarProvider and the corresponding .Root into one component — ideal for the common case:
<MonthView temporal={Temporal} selectionMode="range">
{/* navigation + grid */}
</MonthView>
Use the explicit form when you need to place the provider yourself — for example, to share one selection between two views, or to read calendar state from components that live outside the view:
<CalendarProvider temporal={Temporal} selectionMode="range">
<MonthView.Root numberOfMonths={2}>{/* grids */}</MonthView.Root>
<SelectionSummary /> {/* reads state via hooks, outside the view */}
</CalendarProvider>
Both forms accept the same props; the wrapper simply forwards view props to .Root and everything else to the provider.
The render prop
Every component accepts a render prop (the same pattern as Base UI) for when className and CSS aren't enough. Pass a function that receives the merged DOM props and a typed state object, and return the element you want:
<WeekTemplate
render={(props, state) => (
<tr
{...props}
style={state.gridRowIndex ? { gridRow: state.gridRowIndex } : undefined}
/>
)}
/>
Rules of thumb:
- Always spread
propsonto your element — they carry the event handlers, ARIA attributes, anddata-*state the library manages. stateis read-only and fully typed per component (DayButtonState,WeekTemplateState,RangeSelectedState, …). Every state also includesstate.root(aRootState) with calendar-wide values:selected,rangeStart/rangeEnd,viewing,focused,locale,timeZone, and more.- Keep the element type semantically equivalent (a
<td>for cell parts, a<button>for interactive parts) so the grid semantics survive.
Building your own parts
When the shipped parts aren't enough, use the same hooks and contexts they're built from:
useCalendarStable()— stable config and actions:onSelect(date),setRange(start, end),selectionMode,minValue/maxValue,temporal,locale,timeZone,weekStartDay.useCalendarState()— live selection state:selected,selectedDates,rangeStart/rangeEnd,hoveredDate,previewStart/previewEnd.useMonthViewState()/useWeeksViewState()— view state: the visible month data (allMonths) or the weeks window (windowInfo).DayCellDataContext/GridContext— inside a cell, the cell'sdateand the grid'sorientation.
For example, a footer that shows the current selection:
function SelectionSummary() {
const { locale } = useCalendarStable();
const { rangeStart, rangeEnd } = useCalendarState();
if (!rangeStart) return <p>Select a start date</p>;
return (
<p>
{rangeStart.toLocaleString(locale)}
{rangeEnd ? ` – ${rangeEnd.toLocaleString(locale)}` : " – …"}
</p>
);
}
Any component using these hooks must be rendered inside the provider (and inside the view root for the view hooks).
Multiple months
One MonthView.Root can display several consecutive months. Set numberOfMonths, then render one Grid per month with monthIndex:
import { Temporal } from "@js-temporal/polyfill";
import {
MonthView,
PrevMonthButton,
MonthYearString,
NextMonthButton,
Grid,
GridHeader,
GridHeaderCell,
GridBody,
WeekTemplate,
DayCellTemplate,
DayButton,
} from "@klinking/colander";
function MonthGrid({ monthIndex }: { monthIndex: number }) {
return (
<div>
<MonthYearString className="calendar-title" monthIndex={monthIndex} />
<Grid className="calendar-grid" monthIndex={monthIndex}>
<GridHeader>
<GridHeaderCell className="calendar-weekday" />
</GridHeader>
<GridBody>
<WeekTemplate>
<DayCellTemplate>
<DayButton className="calendar-day" />
</DayCellTemplate>
</WeekTemplate>
</GridBody>
</Grid>
</div>
);
}
export function TwoMonthCalendar() {
return (
<MonthView temporal={Temporal} selectionMode="range" numberOfMonths={2}>
<div className="calendar">
<div className="calendar-header">
<PrevMonthButton className="calendar-nav">‹</PrevMonthButton>
<NextMonthButton className="calendar-nav">›</NextMonthButton>
</div>
<div className="calendar-months">
<MonthGrid monthIndex={0} />
<MonthGrid monthIndex={1} />
</div>
</div>
</MonthView>
);
}MonthYearString monthIndex={i} labels each grid; navigation buttons page the whole window. See MonthView for details.