Guides
Components
The six rules that cover the whole compound component: plural names, scopes, as, labels and data attributes.
The tree
Calendar is a compound component built on the same two hooks. The tedious half comes already wired — geometry, scoping, keys — and everything you can see is yours to write. Nothing here is closed off; the pieces arrive pre-wired, not locked down.
import { createCalendarComponents } from '@midstem/chronous-react' import type { CalendarRange, EventInput } from '@midstem/chronous-react' type EventData = { title: string } // Binds your event data to every render prop — see Typed event data below. const Calendar = createCalendarComponents<EventData>() const range: CalendarRange = { view: 'week', currentDate: '2026-03-18', timeZone: 'Europe/Kyiv', } const events: EventInput<EventData>[] = [ { id: 'offsite', start: '2026-03-18', end: '2026-03-20', data: { title: 'Offsite' }, }, { id: 'standup', start: '2026-03-18T09:00', duration: 'PT30M', data: { title: 'Standup' }, }, ] export const Board = () => ( <Calendar.Root className="calendar" range={range} events={events} locale="en-GB" > <Calendar.Header className="grid-header"> <Calendar.DayHeadings className="heading"> {({ weekdayLabel, dayLabel }) => ( <> <span>{weekdayLabel}</span> <strong>{dayLabel}</strong> </> )} </Calendar.DayHeadings> </Calendar.Header> <Calendar.AllDayRow gutterCell="all-day"> <Calendar.AllDayEvents className="bar"> {({ event }) => event.data?.title} </Calendar.AllDayEvents> </Calendar.AllDayRow> <Calendar.TimeGrid hourHeight={60}> <Calendar.TimeAxis className="gutter"> <Calendar.TimeLabels className="tick" /> </Calendar.TimeAxis> <Calendar.DayColumns className="column"> <Calendar.TimeSlots className="line" /> <Calendar.NowMarker className="now" /> <Calendar.TimedEvents as="button" className="event"> {({ event }) => event.data?.title} </Calendar.TimedEvents> </Calendar.DayColumns> </Calendar.TimeGrid> </Calendar.Root> )
MonthGrid / MonthRows / MonthDays cover the month view and AgendaList / AgendaDays the agenda — the full list is in the API reference.
Six rules cover the whole surface
A plural name iterates
DayColumns renders one element per day, TimedEvents one per box, TimeSlots one per slot. The repetition is the engine’s, not yours, so the component owns the loop and the keys. Singular names — Root, Header, TimeGrid, NowMarker — render once.
Children are a node or a function of the scope
Either way they render inside that scope, so nested components resolve.
import { Calendar } from '@midstem/chronous-react' import type { CalendarRange, EventInput } from '@midstem/chronous-react' const range: CalendarRange = { view: 'month', currentDate: '2026-03-18', timeZone: 'Europe/Kyiv', } const events: EventInput[] = [ { id: 'review', start: '2026-03-18T14:00', duration: 'PT1H' }, ] export const Month = () => ( <Calendar.Root range={range} events={events} locale="en-GB"> <Calendar.MonthGrid className="month"> <Calendar.MonthRows className="row"> {/* MonthTimedEvents resolves because it renders inside the cell */} <Calendar.MonthDays className="day"> {({ dayLabel, inCurrentPeriod }) => ( <div data-outside={!inCurrentPeriod}> {dayLabel} <Calendar.MonthTimedEvents className="dot" /> </div> )} </Calendar.MonthDays> </Calendar.MonthRows> </Calendar.MonthGrid> </Calendar.Root> )
Every scope is also a hook — useDayColumnContext, useMonthRowContext, useAgendaDayContext and the rest — so a component of your own can sit inside a part and read it without a render prop. Reading a scope outside its parent throws and names the parent it wants.
import { Calendar, useDayColumnContext } from '@midstem/chronous-react' import type { CalendarRange, EventInput } from '@midstem/chronous-react' const range: CalendarRange = { view: 'week', currentDate: '2026-03-18', timeZone: 'Europe/Kyiv', } const events: EventInput[] = [ { id: 'standup', start: '2026-03-18T09:00', duration: 'PT30M' }, ] // A component of your own: no render prop, no props threaded down. const DayTotal = () => { const { day } = useDayColumnContext() return <span className="total">{day.boxes.length}</span> } export const Board = () => ( <Calendar.Root range={range} events={events}> <Calendar.TimeGrid hourHeight={48}> <Calendar.DayColumns className="column"> <DayTotal /> <Calendar.TimedEvents className="event" /> </Calendar.DayColumns> </Calendar.TimeGrid> </Calendar.Root> )
as picks the tag, and your style wins
Every component forwards className, ref, handlers and aria-* to the element it renders, and merges the layout it computed underneath the style you pass — so an event can be a button, and a top of your own overrides the one the engine placed.
A formatted string ends in Label
weekdayLabel, dayLabel, monthLabel, timeLabel and timeRangeLabel have already been through formatIso in the calendar’s locale. Everything without the suffix is data: day is a CalendarDay, box a CalendarBox, bar a CalendarBar.
Per-item state arrives as data attributes
className is shared by every element a component renders, so per-item state lands as attributes instead. data-date and data-in-current-period land on DayHeadings, DayColumns, MonthDays and AgendaDays; data-event-id and data-continues-before / data-continues-after land on the event components. A prop you pass wins over the attribute.
import { Calendar } from '@midstem/chronous-react' import type { CalendarRange } from '@midstem/chronous-react' const range: CalendarRange = { view: 'month', currentDate: '2026-03-18', timeZone: 'Europe/Kyiv', } // className is shared by every cell, so the per-day state is an attribute. export const Month = () => ( <Calendar.Root range={range} events={[]} locale="en-GB"> <Calendar.MonthGrid className="month"> <Calendar.MonthRows className="row"> <Calendar.MonthDays className="day" /> </Calendar.MonthRows> </Calendar.MonthGrid> </Calendar.Root> )
.day[data-in-current-period='false'] { opacity: 0.4; } /* the same thing with Tailwind, on the component itself */ /* <Calendar.MonthDays className="data-[in-current-period=false]:opacity-40" /> */
The gutter lives on Root
Header, AllDayRow and TimeGrid lay out the same CSS grid, so gutterWidth is one prop on the root rather than three that can drift apart. Month and agenda ignore it. What goes in that leading column is gutterCell, on Header and on AllDayRow.
Typed event data
Context cannot infer a type argument, so Calendar on its own hands render props data?: unknown. createCalendarComponents binds the namespace once and the type flows to every render prop.
import { createCalendarComponents } from '@midstem/chronous-react' import type { CalendarRange, EventInput } from '@midstem/chronous-react' type EventData = { title: string; owner: string } // A cast, not a factory — it costs nothing and lives at module scope. const Calendar = createCalendarComponents<EventData>() const range: CalendarRange = { view: 'day', currentDate: '2026-03-18', timeZone: 'Europe/Kyiv', } const events: EventInput<EventData>[] = [ { id: 'standup', start: '2026-03-18T09:00', duration: 'PT30M', data: { title: 'Standup', owner: 'Ada' }, }, ] export const Board = () => ( <Calendar.Root range={range} events={events}> <Calendar.TimeGrid hourHeight={48}> <Calendar.DayColumns className="column"> <Calendar.TimedEvents className="event"> {/* event.data is EventData here, not unknown */} {({ event }) => `${event.data?.title} · ${event.data?.owner}`} </Calendar.TimedEvents> </Calendar.DayColumns> </Calendar.TimeGrid> </Calendar.Root> )