Event Calendar
A month grid that shows events on the days they happen. People move between months, step through days with the arrow keys, pick an event, and add one from a day when the app allows it. The app owns the events and the month. Days with more events than fit show a +N more button that opens the full list for that day. UI permission is not security: the server must check again.
A ready-made piece, built from the library's own parts.
A block puts several components together into something you would otherwise assemble by hand. It never fetches data: you give it the data, or answer its callbacks. It is copied into your project like any component, with the parts it uses, so you can change anything.
Basic
October 2026
Nothing picked yet
States
October 2026
Loading events
October 2026
October 2026
Permissions
Can create
October 2026
Read only
October 2026
Week starts monday
Oktober 2026
Installation
npx rdloom add event-calendarCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs @internationalized/date, react-aria-components; add --install to install them.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/event-calendar.jsonWorks, but without upgrade tracking.
Usage
import { EventCalendar, type EventCalendarEvent } from "@rdloom/react";
<div className="flex w-full justify-center">
<div className="flex w-full max-w-4xl flex-col gap-3">
<EventCalendar
events={events}
defaultMonth="2026-10"
today="2026-10-08"
onEventSelect={(event) => setPicked(`Event: ${event.title}`)}
onDateSelect={(date) => setPicked(`Day: ${date}`)}
/>
<p role="status" className="text-sm text-[var(--rd-color-text-muted)]">
{picked}
</p>
</div>
</div>API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
eventsrequiredThe events. start and end are ISO strings (2026-10-12 or 2026-10-12T09:30); the date part places the event, the time part is shown. An event with an end on a later day appears on each day it covers. | Array<{ id: string; title: string; start: string; end?: string; tone?: "neutral" | "info" | "success" | "warning" | "danger"; allDay?: boolean }> | none |
monthThe month shown, as YYYY-MM. Use it with onMonthChange to control the month. | string | none |
defaultMonthThe month shown first when the month is not controlled, as YYYY-MM. Default: the current month. | string | none |
onMonthChangeCalled with YYYY-MM when someone moves to another month, so you can load that month's events. | (month: string) => void | none |
weekStartsOnThe first day of the week, 0 for Sunday to 6 for Saturday. | number | 0 |
localeA BCP 47 locale for month, day and time names, e.g. "de-DE". Default: en-US on the server and during hydration, then the browser's. | string | none |
todayThe date treated as today, as YYYY-MM-DD. Default: the device's date. Set it for stable screenshots and tests. | string | none |
maxEventsPerDayHow many events a day shows before the rest go behind "+N more". | number | 3 |
labelNames the grid for screen readers, together with the month. | string | "Calendar" |
onEventSelectCalled when someone picks an event. | (event: { id: string; title: string; start: string; end?: string; tone?: "neutral" | "info" | "success" | "warning" | "danger"; allDay?: boolean }) => void | none |
onDateSelectCalled with YYYY-MM-DD when someone picks a day (click, Enter or Space). | (date: string) => void | none |
onCreateCalled with YYYY-MM-DD when someone uses a day's add button. The button shows only when this is set and permissions.create allows it. | (date: string) => void | none |
stateloading shows a skeleton grid and marks the calendar busy, empty says there are no events, error says they could not load. ready or no state shows the month. | DataState | none |
onRetryCalled by the Try again button in the error state. | () => void | none |
permissionsWhat the app allows: create. Hidden leaves the add buttons out; disabled keeps them reachable (aria-disabled) with the reason read, and nothing runs. This only changes what people see: the server must check again. | Permissions<"create"> | none |
classNamesExtra class names for single parts, so you can restyle one part without editing the file. Keys: root, header, title, nav, grid, weekday, day, dayNumber, event, more, addButton, popover. | Partial<Record<"root" | "header" | "title" | "nav" | "grid" | "weekday" | "day" | "dayNumber" | "event" | "more" | "addButton" | "popover", string>> | none |
Accessibility
Role grid, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Tab: moves into the grid on one day, then to the controls inside that day
- Arrow keys: move between days, across month edges
- Home and End: first and last day of the week
- Page Up and Page Down: previous and next month
- Enter or Space: picks the day
- Escape: closes the +N more list
Screen readers announce
- "Thursday 8 October 2026, 2 events, grid cell"
- "9:00 AM Design review, button"
- "3 more events on 8 October, button"
What your code must do
- A grid with column headers for the weekdays and one row per week
- Only one day is in the tab order; the arrow keys move it (roving tabindex)
- Each day names its full date and how many events it has
- Events are buttons named by time and title
- The month heading is announced when the month changes
- A disabled add button stays focusable and its reason is read
- Event tone is shown by a dot and the color, never the only cue to the title
Block contract
- Data
- A list of events with ISO start and optional end, and a month.
- Data states
- loading, empty, error, ready
- Permissions
- create
- Events
- onMonthChange, onEventSelect, onDateSelect, onCreate, onRetry
- You can replace
- classNames for each part; weekStartsOn and locale; maxEventsPerDay; month or defaultMonth
Guidelines
Use it when
- Showing what is scheduled across a month: bookings, deliveries, releases
- Letting people open an event or start a new one from a day
Avoid it when
- Picking one date for a form: use DatePicker or Calendar
- Choosing a time on a day: use TimeSlotPicker
- Hour by hour scheduling with overlapping events: this shows a month, not a day timeline
Don't
- Treating the permissions prop as protection: UI permission is not security, so the server must check again
- Loading every event the app has: load the visible month and use onMonthChange
- Telling event types apart by color alone: put the type in the title
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-surface-default--rd-color-surface-subtle--rd-color-surface-selected--rd-color-border-default--rd-color-text-default--rd-color-text-muted--rd-color-action-primary--rd-color-action-on-primary--rd-color-focus-ring--rd-color-feedback-info--rd-color-feedback-info-subtle--rd-color-feedback-success--rd-color-feedback-success-subtle--rd-color-feedback-warning--rd-color-feedback-warning-subtle--rd-color-feedback-danger--rd-color-feedback-danger-subtle