Components
WeeksView
A continuously scrolling window of week rows that spans month boundaries.
WeeksView renders a fixed-height window of week rows that scrolls continuously through the calendar — no page flips, no month boundaries. It's the layout behind agenda-style mini calendars (think Google Calendar's sidebar): June's last week and July's first week can sit next to each other in the same view.
Compared to MonthView:
| MonthView | WeeksView | |
|---|---|---|
| Unit of display | Whole months | Any consecutive weeks |
| Navigation | Page by month | Scroll by week row or page |
| Window height | Rows per month (or fixedWeeks) | Always exactly weekCount rows |
| Month labels | One per grid | MonthSeparator parts inside the flow |
Both views share the same grid parts, selection model, and provider — switching between them is mostly a matter of swapping the root and navigation.
import { Temporal } from "@js-temporal/polyfill";
import {
WeeksView,
PrevWeeksButton,
NextWeeksButton,
Grid,
GridHeader,
GridHeaderCell,
GridBody,
WeekTemplate,
DayCellTemplate,
DayButton,
useWeeksViewState,
} from "@klinking/colander";
function VisibleMonthsLabel() {
const { windowInfo } = useWeeksViewState();
const label = windowInfo.visibleMonths
.map(({ month, year }) =>
new Date(year, month - 1).toLocaleDateString("en-US", {
month: "short",
year: "numeric",
}),
)
.join(" – ");
return <span className="calendar-title">{label}</span>;
}
export function BasicWeeksView() {
return (
<WeeksView
temporal={Temporal}
weekCount={6}
defaultFirstWeek={{ month: 6, year: 2026 }}
scrollBy="row"
>
<div className="calendar">
<div className="calendar-header">
<PrevWeeksButton className="calendar-nav">↑</PrevWeeksButton>
<VisibleMonthsLabel />
<NextWeeksButton className="calendar-nav">↓</NextWeeksButton>
</div>
<Grid className="calendar-grid">
<GridHeader>
<GridHeaderCell className="calendar-weekday" />
</GridHeader>
<GridBody>
<WeekTemplate>
<DayCellTemplate>
<DayButton className="calendar-day" />
</DayCellTemplate>
</WeekTemplate>
</GridBody>
</Grid>
</div>
</WeeksView>
);
}The window
weekCount(required) — how many week rows are visible.firstWeek/defaultFirstWeek/onFirstWeekChange— the controlled / uncontrolled first visible week. Any date-like value works — aFirstWeekSpecis resolved to the containing week and snapped toweekStartDay:
<WeeksView weekCount={5} defaultFirstWeek={{ month: 6, year: 2026 }} />
// also accepted:
// Temporal.PlainDate.from("2026-06-15")
// new Date(2026, 5, 15)
// { isoWeek: 25, isoYear: 2026 }
// { week: 25, year: 2026 } (relative to weekStartDay)
onWindowChange— fires with aWindowInfosnapshot whenever the window moves:windowStart/windowEnd, day and week counts, how many of those are enabled, and thevisibleMonthslist — handy for rendering a "Jun – Jul 2026" heading (see the example above, which reads the same data fromuseWeeksViewState().windowInfo).
Scrolling
PrevWeeksButton/NextWeeksButtonshift the window;scrollBydecides the step —"row"(one week, default) or"page"(a fullweekCount).WeekCountrenders the number of visible weeks, if you want it in your UI.- Keyboard: arrowing or paging focus past the window edge scrolls it automatically.
- Imperative scrolling —
WeeksView(andWeeksView.Root) forwards a ref with ascrollToWeek(target, { snap })handle:
const ref = useRef<WeeksViewRootHandle>(null);
<WeeksView ref={ref} temporal={Temporal} weekCount={6}>
{/* … */}
</WeeksView>;
// later:
ref.current?.scrollToWeek(Temporal.PlainDate.from("2026-09-01"), {
snap: "center",
});
snap positions the target within the window: "start" (default), "center", "end", or "nearest" — which scrolls only if the target is outside the window, choosing the closer edge.
Behavior at the bounds
With min/max set, outOfRangeBehavior controls how the window treats the bounds (selection is always restricted regardless):
| Value | Behavior |
|---|---|
"unbounded" (default) | Scroll freely; out-of-range days render disabled |
"stop" | Nav buttons disable once the next step would show no in-range day |
"stop-shrink" | Like "stop", but the window shrinks near the edge instead of showing fully-disabled rows |
"snap" | Overshooting jumps snap the window edge to the first/last in-range week |
"snap-shrink" | Snap, then trim any remaining fully-disabled rows |
"snap" and "snap-shrink" only differ when the selectable range spans fewer weeks than weekCount: snapping can pin only one edge, so the window overhangs the other — "snap" keeps the full height (padding with disabled rows) while "snap-shrink" trims to just the in-range weeks. With weekCount: 6 and bounds spanning 2 weeks, "snap" shows 6 rows (4 disabled), "snap-shrink" shows 2.
Month separators
Because months flow into each other, MonthSeparator parts let you mark where a new month begins inside the grid — a border above its first week, a rotated month label in a side column, however you like. MonthSeparatorRow repeats for each month whose first day is in view and exposes layout state (firstDayColumn, firstDayVisible, gridRowStart, fullWeeksVisibleAfter) via its render prop, with MonthSeparatorMonth / MonthSeparatorYear for localized labels:
<GridBody>
<MonthSeparatorRow
render={(props, state) => (
<tr {...props} className="contents">
<td className="contents">
{state.firstDayVisible && (
<div
className="month-rule"
style={{
gridRow: state.gridRowStart,
gridColumn: `${state.firstDayColumn + 1} / -1`,
}}
>
<MonthSeparatorMonth format="short" />
</div>
)}
</td>
</tr>
)}
/>
<WeekTemplate>{/* … */}</WeekTemplate>
</GridBody>
Like the range overlays, separators assume a CSS-grid layout on Grid — see Styling and the demo source for a complete implementation.
Week numbers
WeekNumberHeader / WeekNumberCell work exactly as in MonthView.
API reference
Props
WeeksView accepts all CalendarProvider props plus the view props below.
| Prop | Type | Default | Description |
|---|---|---|---|
| weekCount* | number | — | Number of week rows to display simultaneously. |
| firstWeek | FirstWeekSpec | undefined | — | The controlled first visible week. When provided, the component is controlled. |
| defaultFirstWeek | FirstWeekSpec | undefined | — | The initial first visible week (uncontrolled). |
| onFirstWeekChange | ((date: PlainDate) => void) | undefined | — | Called when the first visible week changes via navigation or focus movement. |
| scrollBy | "row" | "page" | undefined | "row" | How much to scroll per navigation step.
- "row" — scroll one week row at a time.
- "page" — scroll a full page (all visible rows) at a time. |
| outOfRangeBehavior | OutOfRangeBehavior | undefined | "unbounded" | How the visible window behaves at the min/max bounds. min/max
always restrict which **days are selectable**; this controls whether the
window may scroll beyond them, and how the edge is handled. See
OutOfRangeBehavior.
- "unbounded" — navigation is never restricted by min/max.
- "stop" — navigation halts once the next step has no in-range day.
- "stop-shrink" — like "stop", but the window shrinks at the edge.
- "snap" — overshooting jumps snap the window edge to the last/first
in-range week.
- "snap-shrink" — snap to the boundary, shrinking if still out of range. |
| onWindowChange | ((info: WindowInfo) => void) | undefined | — | Called when the visible window changes. |
| children | React.ReactNode | — | React children. |
Window info
| Prop | Type | Default | Description |
|---|---|---|---|
| windowStart* | PlainDate | — | The first date of the visible window. |
| windowEnd* | PlainDate | — | The last date of the visible window. |
| weekCount* | number | — | Number of week rows in the window (the prop value, not actual when shrunk). |
| dayCount* | number | — | Total number of calendar days in the window. |
| enabledWeekCount* | number | — | Number of weeks that contain at least one enabled date. |
| enabledDayCount* | number | — | Number of enabled (selectable) dates in the window. |
| visibleMonths* | VisibleMonth[] | — | Distinct months visible in the window, in chronological order. |
First week spec
| Prop | Type | Default | Description |
|---|---|---|---|
| toLocaleString* | (() => string) | ((locales?: string | string[] | undefined, options?: Intl.DateTimeFormatOptions | undefined) => string) | { (): string; (locales?: string | string[] | undefined, options?: Intl.DateTimeFormatOptions | undefined): string; (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions | undefined): string; } | — | |
| toString* | ((options?: Temporal.ShowCalendarOption | undefined) => string) | (() => string) | (() => string) | — | |
| valueOf* | (() => never) | (() => number) | (() => Object) | — |
Members
PlainDateDate{ isoWeek: number; isoYear: number; }{ week: number; year: number; }{ month: number; year: number; day?: number; }Hooks
Inside a WeeksView, useWeeksViewStable() and useWeeksViewState() expose the view's context — including windowInfo and the scrollToWeek/goNext/goPrev actions — for custom components.