Components

MonthView

The traditional paged month grid, with multi-month layouts and month-level navigation.

MonthView displays one or more month grids and pages between them — the classic date picker layout. It manages the visible month(s), keyboard focus, and navigation; selection state comes from its CalendarProvider.

Use MonthView directly for the common case (it wraps a provider for you), or CalendarProvider + MonthView.Root when you compose the provider yourself:

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>
  );
}
  • PrevMonthButton / NextMonthButton page the view one month at a time and disable themselves at the bounds when outOfRangeBehavior="stop".
  • MonthYearString renders the localized current month label (and labels the grid for assistive tech).
  • Keyboard: PageUp/PageDown page months, Shift+PageUp/Shift+PageDown page years, and arrowing past the grid edge moves the view automatically (full list).

Controlled month

By default the view manages the visible month itself, starting at the selection or today (defaultMonth overrides the start). To control it — syncing with a URL, an agenda pane, or a "jump to date" input — pass month and update it in onMonthChange:

tsx
const [month, setMonth] = useState(() =>
  Temporal.PlainYearMonth.from("2026-06"),
);

<MonthView temporal={Temporal} month={month} onMonthChange={setMonth}>
  {/* … */}
</MonthView>;

month is an ISO Temporal.PlainYearMonth and round-trips with onMonthChange exactly; onMonthChange also fires for keyboard-driven month crossings, but never on mount.

Multiple months

Set numberOfMonths (1–12) and render one Grid monthIndex={i} per month. Navigation still moves by single months, revealing one new month per click:

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>
  );
}

Give each grid its own label with MonthYearString monthIndex={i}. Range selection spans grids naturally — a range can start in one month and end in another.

Grid shape

  • fixedWeeks — always render 6 week rows, padding with adjacent-month days. February 2026 (4 rows) and August 2026 (6 rows) take the same height, so nothing below the calendar jumps as users page.
  • outsideDays — what to do with the adjacent-month days that pad the first and last weeks:
ValueBehavior
"enabled" (default)Fully interactive; clicking one selects it
"readOnly"Visible but not selectable; range highlighting still paints through
"disabled"Visible but not selectable; no range highlighting
"hidden"Blank cells (kept in the DOM with aria-hidden for a stable grid shape)

Outside days carry data-outside-month (and data-hidden when hidden) for styling.

Bounds

min/max disable out-of-range days everywhere. outOfRangeBehavior additionally decides whether the user can still page past them:

  • "unbounded" (default) — page freely; out-of-range days simply render disabled.
  • "stop"PrevMonthButton/NextMonthButton disable once the destination month crosses a bound.

Week numbers

Add a WeekNumberHeader to the header row and a WeekNumberCell at the start of the week template to show ISO week numbers (determined by each row's Thursday, per ISO 8601):

tsx
<GridHeader>
  <WeekNumberHeader className="weeknum" />
  <GridHeaderCell />
</GridHeader>
<GridBody>
  <WeekTemplate>
    <WeekNumberCell className="weeknum" />
    <DayCellTemplate>
      <DayButton />
    </DayCellTemplate>
  </WeekTemplate>
</GridBody>

API reference

Props

MonthView accepts all CalendarProvider props plus the view props below.

PropTypeDefaultDescription
numberOfMonthsnumber | undefined1Number of months to display simultaneously (1–12).
fixedWeeksboolean | undefinedfalseWhen true, always render 6 week rows per month grid. Prevents layout shifts when navigating between months.
outsideDaysOutsideDays | undefined"enabled"Controls how days from adjacent months are displayed.
outOfRangeBehaviorMonthOutOfRangeBehavior | undefined"unbounded"How month navigation behaves at the min/max bounds. min/max always restrict which **days are selectable**; this controls whether you can still **view** months outside them. See MonthOutOfRangeBehavior. - "unbounded" — Prev/Next are never disabled by min/max; you can page to any month, with out-of-range days rendered disabled. - "stop" — Prev/Next disable once the destination month crosses the bound.
monthPlainYearMonth | undefinedThe controlled visible month. When provided, the component is controlled. Interpreted in the ISO calendar (the year/month are read as ISO values). The locale only affects how the month is displayed, so this value round-trips directly with onMonthChange.
defaultMonthPlainYearMonth | undefinedThe initial visible month (uncontrolled). Interpreted in the ISO calendar.
onMonthChange((month: PlainYearMonth) => void) | undefinedCalled when the visible month changes via navigation or focus movement. Not called on initial mount. The argument is an ISO PlainYearMonth.
childrenReact.ReactNodeReact children.

Hooks

Inside a MonthView, useMonthViewStable() and useMonthViewState() expose the view's context for custom components:

PropTypeDefaultDescription
numberOfMonths*numberNumber of simultaneously visible months.
fixedWeeks*booleanWhether month grids always render 6 week rows.
outsideDays*OutsideDaysHow outside-month days are displayed.
outOfRangeBehavior*MonthOutOfRangeBehaviorHow month navigation behaves at bounds.
goNextMonth*() => voidNavigate to the next month(s).
goPrevMonth*() => voidNavigate to the previous month(s).
setGridLabelId*(monthIndex: number, id: string | undefined) => voidRegister (or clear) the id of a label element for aria-labelledby, keyed by month index.
gridFocusedRef*React.RefObject<boolean>Ref tracking whether the grid currently holds DOM focus (avoids state re-renders).
PropTypeDefaultDescription
currentMonth*{ year: number; month: number; }The primary displayed month (year + month).
weeks*PlainDate[][]2D array of weeks for the first visible month grid.
allMonths*MonthData[]Pre-computed data for all visible months (length = numberOfMonths).
currentDateTime*PlainDateTimeDate-time representing the viewed month with time from the selection (for "today" highlighting).
gridLabelIds*Record<number, string>Map of month index to label element id (for per-grid aria-labelledby).
rootState*RootStateThe root component's state object for render functions.