Overview
Quick start
Install Colander and build a working, styled calendar in a few minutes.
This walkthrough takes you from an empty file to a working, styled month calendar.
Install the package
Install @klinking/colander and a Temporal polyfill in your React project:
npm install @klinking/colander @js-temporal/polyfillColander has three peer dependencies — react, react-dom (18+), and @base-ui/react — which your package manager installs automatically. The Temporal polyfill is explicit on purpose: the library doesn't bundle one, so you control which implementation you ship (see Dates & formats for the options).
Releases are currently published to the alpha dist-tag while the API stabilizes — @klinking/colander@alpha installs the latest prerelease.
Provide Temporal
Every calendar needs a Temporal implementation. Import it once and pass it via the temporal prop:
import { Temporal } from "@js-temporal/polyfill";
<MonthView temporal={Temporal}>{/* … */}</MonthView>;
If the runtime already exposes the native Temporal global, the prop can be omitted.
Assemble the calendar
Colander components are compound parts you compose in JSX, so your markup mirrors the calendar's actual structure. Here is a complete single-month calendar with navigation:
import { Temporal } from "@js-temporal/polyfill";
import {
MonthView,
PrevMonthButton,
MonthYearString,
NextMonthButton,
Grid,
GridHeader,
GridHeaderCell,
GridBody,
WeekTemplate,
DayCellTemplate,
DayButton,
} from "@klinking/colander";
export function BasicCalendar({
onSelect,
}: {
onSelect?: (value: Temporal.PlainDate | null) => void;
}) {
return (
<MonthView temporal={Temporal} onValueChange={onSelect}>
<div className="calendar">
<div className="calendar-header">
<PrevMonthButton className="calendar-nav">‹</PrevMonthButton>
<MonthYearString className="calendar-title" />
<NextMonthButton className="calendar-nav">›</NextMonthButton>
</div>
<Grid className="calendar-grid">
<GridHeader>
<GridHeaderCell className="calendar-weekday" />
</GridHeader>
<GridBody>
<WeekTemplate>
<DayCellTemplate>
<DayButton className="calendar-day" />
</DayCellTemplate>
</WeekTemplate>
</GridBody>
</Grid>
</div>
</MonthView>
);
}A quick tour of the parts:
MonthView— the all-in-one root. It owns selection state and month navigation, and reports selection throughonValueChange. (Under the hood it composes a CalendarProvider with aMonthView.Root— you can also use those two directly when you need more control.)PrevMonthButton/NextMonthButton—<button>s that page the visible month and disable themselves at yourmin/maxbounds.MonthYearString— a localized "June 2026" label, wired to the grid viaaria-labelledbyand announced politely when the month changes.Grid,GridHeader,GridHeaderCell,GridBody— the calendar table. A singleGridHeaderCellwith noindexrenders all seven localized weekday headers.WeekTemplate/DayCellTemplate/DayButton— templates: you write one row, one cell, and one button, and the library stamps them out for every week and day in view.
Read the selection
onValueChange receives the new value in your configured format — a Temporal.PlainDate | null by default:
function BookingForm() {
const [date, setDate] = useState<Temporal.PlainDate | null>(null);
return (
<>
<BasicCalendar onSelect={setDate} />
<p>{date ? date.toLocaleString() : "Pick a date"}</p>
</>
);
}
The example above is uncontrolled (the calendar keeps its own state). To control it, pass value and update it from onValueChange — see Selection modes for the full rules, plus range and multi-select.
Style it
Nothing you've rendered so far has any appearance — that's yours. Components accept className like any element, and they expose their interaction state as data-* attributes, so most styling is plain CSS:
.calendar {
inline-size: 20rem;
font: inherit;
}
.calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-block-end: 0.5rem;
}
.calendar-nav {
inline-size: 2rem;
block-size: 2rem;
border: none;
border-radius: 0.375rem;
background: transparent;
cursor: pointer;
}
.calendar-nav:hover {
background: #f1f0ef;
}
.calendar-nav:disabled {
opacity: 0.4;
cursor: default;
}
.calendar-grid {
inline-size: 100%;
border-collapse: collapse;
}
.calendar-weekday {
padding-block: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
color: #6f6d66;
}
.calendar-day {
inline-size: 2.25rem;
block-size: 2.25rem;
border: none;
border-radius: 0.375rem;
background: transparent;
cursor: pointer;
}
.calendar-day:hover {
background: #f1f0ef;
}
.calendar-day[data-today] {
font-weight: 700;
}
.calendar-day[data-outside-month] {
color: #b5b3ad;
}
.calendar-day[data-selected] {
background: #1a1a17;
color: #fff;
}
.calendar-day[data-disabled] {
opacity: 0.4;
cursor: default;
}
The Styling guide covers the complete data-attribute reference, Tailwind usage, and the render prop for cases where CSS alone isn't enough.
Next steps
- Composition — the component tree, templates, and the
renderprop. - Selection modes — single, multiple, and range selection.
- Dates & formats — value formats, time zones, and locales.
- MonthView and WeeksView — everything each view can do.