Source: https://monarchui.dev/components/calendar-month-view/

Components · Date, time and scheduling

# CalendarMonthView

A month grid of days with events, "+N more" popovers and arrow-key movement.

@rookcom/monarch-react/scheduling

Component

## Month with events

## Usage examples

### Product-owned navigation

### Narrow (dot summary)

## 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 `month` and changes it from its own toolbar or from `onNavigate`.
- Busy days stay calm: at most `maxEvents` lines 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

```tsx
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 calls `onEventClick(event)` or follows its `href`.
- "+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.
- `renderEvent` wraps 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-solid` with `--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 `month` in your router or state and change it from `onNavigate` so keyboard users can leave the month.
- Pass the user’s week start and time zone from their settings.
- Use `dayState` for time off and holidays rather than fake all-day events.

### Don't

- Do not raise `maxEvents` to 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](https://monarchui.dev/components/event-chip/): The chips inside each day.
- [MiniMonth](https://monarchui.dev/components/mini-month/): A small month for picking a date or navigating.
- [TimeGrid](https://monarchui.dev/components/time-grid/): Day and week views with times.
- [AgendaList](https://monarchui.dev/components/agenda-list/): A list of the same events by day.
- [Popover](https://monarchui.dev/components/popover/): Hosts the "+N more" list.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [EventChip](https://monarchui.dev/components/event-chip/), [Popover](https://monarchui.dev/components/popover/) |

## 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 `onNavigate` and focus follows the new month.
- `dayState` hatching 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`

## All example code

### Month with events

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

### Product-owned navigation

```tsx
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)

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