Components · Date, time and scheduling
CalendarMonthView
A month grid of days with events, "+N more" popovers and arrow-key movement.
Month with events
import { CalendarMonthView, type CalendarEvent } from '@rookcom/monarch-react/scheduling';
const at = (day: string, time: string) => `${day}T${time}:00Z`;
const EVENTS: CalendarEvent[] = [
{ id: 'm1', title: 'Release day', start: '2026-11-04', allDay: true, color: '#16a34a' },
{
id: 'm2',
title: 'Stand-up',
start: at('2026-11-04', '09:00'),
end: at('2026-11-04', '09:15'),
color: '#0f766e',
},
{
id: 'm3',
title: 'Design review',
start: at('2026-11-04', '11:00'),
end: at('2026-11-04', '12:00'),
color: '#7c3aed',
},
{
id: 'm4',
title: 'Lunch & learn',
start: at('2026-11-04', '12:30'),
end: at('2026-11-04', '13:30'),
color: '#2563eb',
},
{
id: 'm5',
title: 'Retrospective',
start: at('2026-11-04', '16:00'),
end: at('2026-11-04', '17:00'),
status: 'cancelled',
color: '#c2410c',
},
{
id: 'm6',
title: 'Interview',
start: at('2026-11-09', '10:00'),
end: at('2026-11-09', '11:00'),
color: '#0891b2',
},
{
id: 'm7',
title: 'Vendor call',
start: at('2026-11-12', '14:00'),
end: at('2026-11-12', '14:30'),
status: 'tentative',
color: '#2563eb',
},
{
id: 'm8',
title: 'Board meeting',
start: at('2026-11-18', '15:00'),
end: at('2026-11-18', '17:00'),
color: '#db2777',
},
{
id: 'm9',
title: 'Conference',
start: '2026-11-24',
end: '2026-11-26',
allDay: true,
color: '#ca8a04',
},
{
id: 'm10',
title: 'Quarter close',
start: at('2026-11-30', '17:00'),
end: at('2026-11-30', '18:00'),
color: '#7c3aed',
},
];
const base = { timeZone: 'UTC', locale: 'en-GB', today: '2026-11-04', weekStartsOn: 1 } as const;
<div style={{ width: '100%' }}>
<CalendarMonthView
{...base}
month="2026-11-01"
events={EVENTS}
dayState={(d) =>
d === '2026-11-20' ? { hatched: true, badge: 'Holiday' } : undefined
}
/>
</div>Usage examples
Product-owned navigation
import { useState } from 'react';
import { Button } from '@rookcom/monarch-react';
import { CalendarMonthView, type CalendarEvent } from '@rookcom/monarch-react/scheduling';
const at = (day: string, time: string) => `${day}T${time}:00Z`;
const EVENTS: CalendarEvent[] = [
{ id: 'm1', title: 'Release day', start: '2026-11-04', allDay: true, color: '#16a34a' },
{
id: 'm2',
title: 'Stand-up',
start: at('2026-11-04', '09:00'),
end: at('2026-11-04', '09:15'),
color: '#0f766e',
},
{
id: 'm3',
title: 'Design review',
start: at('2026-11-04', '11:00'),
end: at('2026-11-04', '12:00'),
color: '#7c3aed',
},
{
id: 'm4',
title: 'Lunch & learn',
start: at('2026-11-04', '12:30'),
end: at('2026-11-04', '13:30'),
color: '#2563eb',
},
{
id: 'm5',
title: 'Retrospective',
start: at('2026-11-04', '16:00'),
end: at('2026-11-04', '17:00'),
status: 'cancelled',
color: '#c2410c',
},
{
id: 'm6',
title: 'Interview',
start: at('2026-11-09', '10:00'),
end: at('2026-11-09', '11:00'),
color: '#0891b2',
},
{
id: 'm7',
title: 'Vendor call',
start: at('2026-11-12', '14:00'),
end: at('2026-11-12', '14:30'),
status: 'tentative',
color: '#2563eb',
},
{
id: 'm8',
title: 'Board meeting',
start: at('2026-11-18', '15:00'),
end: at('2026-11-18', '17:00'),
color: '#db2777',
},
{
id: 'm9',
title: 'Conference',
start: '2026-11-24',
end: '2026-11-26',
allDay: true,
color: '#ca8a04',
},
{
id: 'm10',
title: 'Quarter close',
start: at('2026-11-30', '17:00'),
end: at('2026-11-30', '18:00'),
color: '#7c3aed',
},
];
const base = { timeZone: 'UTC', locale: 'en-GB', today: '2026-11-04', weekStartsOn: 1 } as const;
function NavigableMonth() {
const [month, setMonth] = useState('2026-11-01');
const label = new Intl.DateTimeFormat('en-GB', {
month: 'long',
year: 'numeric',
timeZone: 'UTC',
}).format(new Date(`${month.slice(0, 7)}-01T12:00:00Z`));
const shift = (n: number) => {
const [y, m] = month.split('-').map(Number) as [number, number];
const d = new Date(Date.UTC(y, m - 1 + n, 1));
setMonth(d.toISOString().slice(0, 10));
};
return (
<div style={{ display: 'grid', gap: 12, width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Button size="sm" onClick={() => shift(-1)}>
Previous
</Button>
<Button size="sm" onClick={() => shift(1)}>
Next
</Button>
<strong aria-live="polite">{label}</strong>
</div>
<CalendarMonthView
{...base}
month={month}
events={EVENTS}
aria-label={label}
onNavigate={(day) => setMonth(day)}
/>
</div>
);
}
<NavigableMonth />Narrow (dot summary)
import { CalendarMonthView, type CalendarEvent } from '@rookcom/monarch-react/scheduling';
const at = (day: string, time: string) => `${day}T${time}:00Z`;
const EVENTS: CalendarEvent[] = [
{ id: 'm1', title: 'Release day', start: '2026-11-04', allDay: true, color: '#16a34a' },
{
id: 'm2',
title: 'Stand-up',
start: at('2026-11-04', '09:00'),
end: at('2026-11-04', '09:15'),
color: '#0f766e',
},
{
id: 'm3',
title: 'Design review',
start: at('2026-11-04', '11:00'),
end: at('2026-11-04', '12:00'),
color: '#7c3aed',
},
{
id: 'm4',
title: 'Lunch & learn',
start: at('2026-11-04', '12:30'),
end: at('2026-11-04', '13:30'),
color: '#2563eb',
},
{
id: 'm5',
title: 'Retrospective',
start: at('2026-11-04', '16:00'),
end: at('2026-11-04', '17:00'),
status: 'cancelled',
color: '#c2410c',
},
{
id: 'm6',
title: 'Interview',
start: at('2026-11-09', '10:00'),
end: at('2026-11-09', '11:00'),
color: '#0891b2',
},
{
id: 'm7',
title: 'Vendor call',
start: at('2026-11-12', '14:00'),
end: at('2026-11-12', '14:30'),
status: 'tentative',
color: '#2563eb',
},
{
id: 'm8',
title: 'Board meeting',
start: at('2026-11-18', '15:00'),
end: at('2026-11-18', '17:00'),
color: '#db2777',
},
{
id: 'm9',
title: 'Conference',
start: '2026-11-24',
end: '2026-11-26',
allDay: true,
color: '#ca8a04',
},
{
id: 'm10',
title: 'Quarter close',
start: at('2026-11-30', '17:00'),
end: at('2026-11-30', '18:00'),
color: '#7c3aed',
},
];
const base = { timeZone: 'UTC', locale: 'en-GB', today: '2026-11-04', weekStartsOn: 1 } as const;
<div style={{ width: '100%', maxWidth: 340 }}>
<CalendarMonthView {...base} month="2026-11-01" events={EVENTS} fixedWeeks={false} />
</div>Purpose
Shows a month at a glance: which days are busy, what is on them, and a way into each day and event. Needed by: calendar, people, stride.
Use when
- The month view of a calendar (bookings, interviews, releases, shifts).
- Planning views where users pick or open a day (attendance, due dates on a board).
Avoid when
- The user needs times and overlaps: use TimeGrid.
- Picking a single date in a form: use DatePicker or MiniMonth.
- A long list of upcoming items: use AgendaList.
Design principles
- A real grid (
role="grid"): weekday column headers, one row per week, arrow keys between days. - Month navigation belongs to the product: it passes
monthand changes it from its own toolbar or fromonNavigate. - Busy days stay calm: at most
maxEventslines per cell, the rest behind "+N more". - Today, outside-month days and hatched days are distinguished by shape and text as well as colour.
- Phones get the same grid with dots instead of chips, not a different component.
API reference
import { CalendarMonthView } from '@rookcom/monarch-react/scheduling';| Export | Kind | Rendering | Root class |
|---|---|---|---|
CalendarMonthView | component | client | .mn-calendar-month-view |
Renders a <div> (ref) holding the role="grid" element. Days are ISO dates ("2026-11-04"); events are CalendarEvent objects read in timeZone.
The focus stop can be controlled (focusedDay, onFocusedDayChange) or uncontrolled (defaultFocusedDay); it defaults to today when in the month, else the 1st.
The overflow list uses Monarch Popover (Radix), labelled by the full date.
Pass timeZone, locale, weekStartsOn and today for identical server and client output; without today the clock is read after mounting.
Generated from the TypeScript declarations of packages/react/src/components/scheduling/calendar-month-view.
Properties
CalendarMonthView
A month of days with their events: weekday header, today and outside-month days marked, a "+N more" popover for busy days, arrow-key movement between days, and callbacks for opening a day or an event. Below about 36rem of container width, cells show a dot summary that opens the day's list.
| Prop | Type | Description |
|---|---|---|
month required | string | Any day in the month to show, `'2026-11-04'`. The product owns month navigation. |
className | string | |
dayState | ((day: string) => CalendarMonthViewDayState | undefined) | Decorate days: hatching and a short badge. |
defaultFocusedDay | string | Initial focus stop (uncontrolled). Default: today when it is in the month, else the 1st. |
events | readonly CalendarEvent[] | |
fixedWeeks | boolean | Always show six weeks, so the height never jumps between months. Default `true`. |
focusedDay | string | The day with the keyboard focus stop (controlled). |
hourCycle | HourCycle | Force a 12- or 24-hour clock (`'12'` or `'24'`, as in the date pickers); by default the locale decides. |
id | string | |
labels | CalendarMonthViewLabels | |
locale | string | BCP 47 locale for weekday, date and time labels. |
maxEvents | number | Events listed in a cell before the "+N more" button. Default 3. |
onDayClick | ((day: string) => void) | Called when a day's date is activated (click, Enter or Space). |
onEventClick | ((event: CalendarEvent) => void) | |
onFocusedDayChange | ((day: string) => void) | |
onNavigate | ((day: string) => void) | Called when keyboard focus moves to a day in another month (arrows into or past the outside days, Page Up / Page Down), with that day. Change `month` to follow it; the day keeps focus. Without it, focus moves only within the shown weeks. |
renderEvent | ((event: CalendarEvent, chip: ReactElement) => ReactNode) | Wrap or replace each chip, e.g. in a PopoverTrigger. Keep one focusable element. |
timeZone | string | IANA zone events are placed in. Defaults to the runtime's; pass it for server rendering. |
today | string | null | Today, `'2026-11-04'`. Inject it for server rendering and tests; when left out it is read from the clock (in `timeZone`) after mounting. `null` marks no day. |
weekStartsOn | Weekday | First column: 0 = Sunday … 6 = Saturday, as in MiniMonth and the date pickers. Default: the locale's week start. |
Variants
| Prop | Values | Description |
|---|---|---|
weekStartsOn | '0''…''6' | First column, 0 = Sunday … 6 = Saturday (the convention MiniMonth and the date pickers use); defaults to the locale’s week start (Intl week info), else Monday. |
fixedWeeks | 'true''false' | true (default) always shows six weeks so the height is stable; false shows only the weeks the month needs. |
maxEvents | 'number' | Lines per cell including the "+N more" button. Default 3. |
States
| State | Exposed as | Behaviour |
|---|---|---|
| Today | data-today, aria-current="date" | Accent-filled date and ", today" in the day’s name. |
| Outside month | data-outside | Subtle background and muted date. |
| Hatched | data-hatched | From dayState: time off or closures, with an optional badge in the name. |
| Focus stop | data-focused | The one day in the tab order; its events and "+N more" are tabbable. |
| Overflow open | aria-expanded="true" | The day’s full list in a popover. |
Interaction behaviour
- Activating a date calls
onDayClick(day); activating an event callsonEventClick(event)or follows itshref. - "+N more" opens a popover with all of the day’s events; Escape or an outside click closes it and focus returns to the button.
- Multi-day events appear in each day they touch; all-day events come first.
renderEventwraps each chip (for example in the product’s event popover).
Keyboard support
| Keys | Action |
|---|---|
| Tab | Moves to the focused day’s date, then its events and "+N more", then out of the grid. |
| ArrowLeft / ArrowRight | Previous or next day (mirrored in right-to-left). |
| ArrowUp / ArrowDown | Same weekday in the previous or next week. |
| Home / End | First or last day of the week. |
| Page Up / Page Down | Same day in the previous or next month (Shift: year), through onNavigate. |
| Enter / Space | Activates the date (onDayClick). |
Moving into another month calls onNavigate(day); the product changes month and focus stays on that day. Without onNavigate, focus moves only within the shown weeks.
Accessibility
- Role
- grid, row, columnheader, gridcell
- Accessible name
- The grid is named by
aria-label(default: the month and year). Each date is named "Wednesday, 4 November 2026, 5 events, today".
- Column headers carry full weekday names for assistive technology and short ones on screen.
- Roving tabindex: one day is in the tab order, so a month is not 42 tab stops.
- The overflow popover is a dialog named by the full date.
- Verified with axe in jsdom and in Chromium, Firefox and WebKit on /components/calendar-month-view.
Responsive behaviour
- A container query switches cells below 36rem to a dot summary (up to three dots and "+N"); the summary button opens the day’s list.
- The grid always fits its container (seven
minmax(0, 1fr)columns); there is no horizontal scrolling at 320px. - Cell height comes from
--mn-calendar-month-view-cell(7rem) and--mn-calendar-month-view-cell-narrow(3.5rem).
Theme support
- Surfaces, borders and text are Monarch tokens; light, dark and more-contrast themes apply automatically.
- Today uses
--mn-color-accent-solidwith--mn-color-accent-on-solid, so it follows the product accent. - Event colours only tint dots and chips (see EventChip).
Do and don't
Do
- Keep
monthin your router or state and change it fromonNavigateso keyboard users can leave the month. - Pass the user’s week start and time zone from their settings.
- Use
dayStatefor time off and holidays rather than fake all-day events.
Don't
- Do not raise
maxEventsto fit everything; the popover is the overflow. - Do not put controls other than events inside cells; open the day for detailed actions.
Related components
- EventChip: The chips inside each day.
- MiniMonth: A small month for picking a date or navigating.
- TimeGrid: Day and week views with times.
- AgendaList: A list of the same events by day.
- Popover: Hosts the "+N more" list.
Dependencies
Testing coverage
- Grid semantics: 7 rows, column headers with full names, 42 cells, outside days, today with
aria-current. - Week start and only-needed weeks.
- All-day first, capped cells, "+N more" popover with every event, event callback, Escape returns focus.
- Day click callback.
- Arrow keys, Home/End, Enter; one tab stop; focus stops at the edge without
onNavigate. - Focused day’s events are tabbable, others are not.
- Page Up/Down call
onNavigateand focus follows the new month. dayStatehatching and badge; right-to-left arrows.today={null}, clock fallback; no axe violations; renders on the server without the clock.
| Check | Covered |
|---|---|
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/calendar-month-view; Open overflow popover: overlays/calendar-month-view-more-light; Cross-browser axe: /components/calendar-month-view |
Tests: packages/react/src/components/scheduling/calendar-month-view