Guides
Layout & lanes
Overlapping events packed into columns, long events promoted to bars, and the “+2 more” cut-off.
Boxes: events inside the grid
Overlapping events are packed into columns the way a calendar draws them, one day at a time. Every box arrives with its position and size already computed, as fractions of the day.
type CalendarBox<TData = unknown> = { event: TimedEntry<TData> start: IsoDateTime end: IsoDateTime startMinute: number endMinute: number minutes: number top: number height: number left: number width: number column: number columns: number span: number continuesBefore: boolean continuesAfter: boolean }
import { buildCalendar } from '@midstem/chronous' import type { EventInput } from '@midstem/chronous' const events: EventInput[] = [ { id: 'a', start: '2026-03-18T09:00', duration: 'PT2H' }, { id: 'b', start: '2026-03-18T09:30', duration: 'PT1H' }, { id: 'c', start: '2026-03-18T14:00', duration: 'PT1H' }, ] const calendar = buildCalendar( { view: 'day', currentDate: '2026-03-18', timeZone: 'Europe/Kyiv' }, events, ) console.log( calendar.days[0].boxes.map(box => [box.event.id, box.left, box.width]), ) // [ [ 'a', 0, 0.5 ], [ 'b', 0.5, 0.5 ], [ 'c', 0, 1 ] ] // a and b overlap and split the day in two; c has nothing beside it.
- Events that overlap form a cluster. The cluster is cut into as few columns as it needs, and each event then widens to the right until it meets a neighbour — an event with nothing beside it takes the full width.
- An event is clipped to every day it touches, so one event crossing midnight is placed once per day with
continuesBeforeandcontinuesAftersaying where it carries on. minutesis the real elapsed length of the clipped piece, which is not always its height: on 29 March 2026 inEurope/Kyivan event from 01:00 to 04:00 is drawn three hours tall and reports two.
import { createCalendarComponents } from '@midstem/chronous-react' import type { CalendarRange, EventInput } from '@midstem/chronous-react' type EventData = { title: string } const Calendar = createCalendarComponents<EventData>() const range: CalendarRange = { view: 'week', currentDate: '2026-03-18', timeZone: 'Europe/Kyiv', } const events: EventInput<EventData>[] = [ // 22:00 to 02:00: one event, placed once on each day it touches { id: 'deploy', start: '2026-03-18T22:00', end: '2026-03-19T02:00', data: { title: 'Deploy window' }, }, ] export const Board = () => ( <Calendar.Root range={range} events={events}> <Calendar.TimeGrid hourHeight={48}> <Calendar.DayColumns className="column"> <Calendar.TimedEvents className="event"> {({ event, box }) => ( <> {box.continuesBefore && '↑ '} {event.data?.title} {box.continuesAfter && ' ↓'} </> )} </Calendar.TimedEvents> </Calendar.DayColumns> </Calendar.TimeGrid> </Calendar.Root> )
Lanes: events above the grid
Long events are drawn as bars above the grid instead of inside it. Every all-day event gets a bar; a timed event gets one when it covers twenty-four hours or more by the wall clock, and it then leaves the grid entirely.
type CalendarBar<TData = unknown> = { event: CalendarEntry<TData> start: IsoDate end: IsoDate startDay: number endDay: number dayCount: number lane: number lanes: number left: number width: number continuesBefore: boolean continuesAfter: boolean }
- A range is cut into lane rows: a month grid breaks at every week, so a bar never crosses a grid row, and every other view is one row.
- Bars read across the row, longest first, and each takes the lowest free lane. Every bar in a row reports the same
lanescount, so a row can be sized before it is drawn. startDayandendDayare day indices inside the row,endDayexclusive, andleft/widthare the same span as fractions of the row.
Overflow, and the empty all-day row
MonthRows takes maxLanes and the laneHeight its bars are drawn at, because both are shared between siblings. MonthAllDayEvents then stops drawing bars past that lane, and every MonthDays cell is handed the bars that cover it — bars for all of them, hiddenBars for the ones the cut-off dropped, and lanes counting only what is drawn. That is the whole “+2 more” affordance.
import { createCalendarComponents } from '@midstem/chronous-react' import type { CalendarRange, EventInput } from '@midstem/chronous-react' type EventData = { title: string } const Calendar = createCalendarComponents<EventData>() const LANE_HEIGHT = 20 const range: CalendarRange = { view: 'month', currentDate: '2026-03-18', timeZone: 'Europe/Kyiv', } const events: EventInput<EventData>[] = ['one', 'two', 'three', 'four'].map( (title, index) => ({ id: title, start: '2026-03-16', end: '2026-03-20', data: { title: `Release ${index + 1}` }, }), ) export const Month = () => ( <Calendar.Root range={range} events={events} locale="en-GB"> <Calendar.MonthGrid className="month"> <Calendar.MonthRows className="row" maxLanes={3} laneHeight={LANE_HEIGHT} > <Calendar.MonthDays className="day"> {({ dayLabel, lanes, hiddenBars }) => ( <> <span>{dayLabel}</span> {/* room for the bars that float above the cells */} <div style={{ height: lanes * LANE_HEIGHT }} /> {hiddenBars.length > 0 && ( <button className="more">+{hiddenBars.length} more</button> )} </> )} </Calendar.MonthDays> <Calendar.MonthAllDayEvents className="bar"> {({ event }) => event.data?.title} </Calendar.MonthAllDayEvents> </Calendar.MonthRows> </Calendar.MonthGrid> </Calendar.Root> )
AllDayRow renders nothing when the range holds no all-day event, which frees the space but moves the grid under it as you step between weeks. minLanes holds the row open instead, and a row that needs more lanes still gets them.
import { Calendar } from '@midstem/chronous-react' import type { CalendarRange, EventInput } from '@midstem/chronous-react' const range: CalendarRange = { view: 'week', currentDate: '2026-03-18', timeZone: 'Europe/Kyiv', } // No all-day event this week — without minLanes the strip would vanish // and the grid below it would jump as you step between weeks. const events: EventInput[] = [ { id: 'standup', start: '2026-03-18T09:00', duration: 'PT30M' }, ] export const Board = () => ( <Calendar.Root range={range} events={events} locale="en-GB"> <Calendar.AllDayRow className="all-day" minLanes={1} gutterCell="all-day"> <Calendar.AllDayEvents className="bar" /> </Calendar.AllDayRow> <Calendar.TimeGrid hourHeight={48}> <Calendar.DayColumns className="column"> <Calendar.TimeSlots className="line" /> <Calendar.TimedEvents className="event" /> </Calendar.DayColumns> </Calendar.TimeGrid> </Calendar.Root> )