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:

MonthViewWeeksView
Unit of displayWhole monthsAny consecutive weeks
NavigationPage by monthScroll by week row or page
Window heightRows per month (or fixedWeeks)Always exactly weekCount rows
Month labelsOne per gridMonthSeparator 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 — a FirstWeekSpec is resolved to the containing week and snapped to weekStartDay:
tsx
<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 a WindowInfo snapshot whenever the window moves: windowStart/windowEnd, day and week counts, how many of those are enabled, and the visibleMonths list — handy for rendering a "Jun – Jul 2026" heading (see the example above, which reads the same data from useWeeksViewState().windowInfo).

Scrolling

  • PrevWeeksButton / NextWeeksButton shift the window; scrollBy decides the step — "row" (one week, default) or "page" (a full weekCount).
  • WeekCount renders 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 scrollingWeeksView (and WeeksView.Root) forwards a ref with a scrollToWeek(target, { snap }) handle:
tsx
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):

ValueBehavior
"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:

tsx
<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.

PropTypeDefaultDescription
weekCount*numberNumber of week rows to display simultaneously.
firstWeekFirstWeekSpec | undefinedThe controlled first visible week. When provided, the component is controlled.
defaultFirstWeekFirstWeekSpec | undefinedThe initial first visible week (uncontrolled).
onFirstWeekChange((date: PlainDate) => void) | undefinedCalled 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.
outOfRangeBehaviorOutOfRangeBehavior | 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) | undefinedCalled when the visible window changes.
childrenReact.ReactNodeReact children.

Window info

PropTypeDefaultDescription
windowStart*PlainDateThe first date of the visible window.
windowEnd*PlainDateThe last date of the visible window.
weekCount*numberNumber of week rows in the window (the prop value, not actual when shrunk).
dayCount*numberTotal number of calendar days in the window.
enabledWeekCount*numberNumber of weeks that contain at least one enabled date.
enabledDayCount*numberNumber of enabled (selectable) dates in the window.
visibleMonths*VisibleMonth[]Distinct months visible in the window, in chronological order.

First week spec

PropTypeDefaultDescription
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.