Event Calendar
Month, week and day views of a schedule. Drag an event to another slot, pull an edge to change its length, or do both from the keyboard.
Installation#
Usage#
import { EventCalendar, type CalendarEvent } from "@/components/ui/event-calendar"
const initial: CalendarEvent[] = [
{ id: "1", title: "Standup", start: new Date(2026, 9, 12, 9, 30), end: new Date(2026, 9, 12, 9, 45), color: "neutral" },
{ id: "2", title: "Offsite", start: new Date(2026, 9, 19), end: new Date(2026, 9, 21), allDay: true, color: "info" },
]
export function Schedule() {
const [events, setEvents] = React.useState(initial)
return (
<EventCalendar
events={events}
onEventChange={(next) => setEvents((all) => all.map((ev) => (ev.id === next.id ? next : ev)))}
/>
)
}Anatomy#
import {
EventCalendar,
EventCalendarHeader,
EventCalendarTitle,
EventCalendarNav,
EventCalendarViewSwitch,
EventCalendarBody,
} from "@/components/ui/event-calendar"
<EventCalendar>
<EventCalendarHeader>
<EventCalendarTitle />
<EventCalendarNav />
<EventCalendarViewSwitch />
</EventCalendarHeader>
<EventCalendarBody />
</EventCalendar>Drag, resize and keys#
In the week and day grid, drag an event to another time or day, or pull its top or bottom edge to change its length, in steps of snapMinutes. Events that overlap sit side by side. The event in hand lifts and glides from step to step, a dashed outline marks where it came from, the events around it stay put, and the grid scrolls when you reach its edge. Escape puts it back. A press without movement is still a click. Nothing changes until you let go: then onEventChange gets the updated event, and you write it back into events. With an event focused, the arrow keys do the same: up and down move it by snapMinutes, left and right by a day, and Shift with up or down changes its length. Click empty space to create an event through onSlotClick.
Month#
Timed events show as a dot, a time and a title; all-day events as a tinted bar, listed first. A day with more than maxEventsPerDay ends in "+N more", which opens that day. Drag an event to another cell, or move it with the arrow keys: a day sideways, a week up or down. Click a day number to open that day.
Day#
One column fits a sidebar or a phone. Pass a single entry to views and the view switch goes away.
Read only#
editable={false} turns off dragging and the keys; events still open through onEventClick. The header is made of parts: this one leaves out the view switch. startHour, endHour and hourHeight fit a working day into a shorter card.
Colours#
color tints the block and its bar with one of the theme's roles, so events stay readable in all four themes. Without it, an event uses brand.
API reference#
EventCalendar
The card holding the schedule, 680px tall (an h-* class sets another height). With no children it renders EventCalendarHeader and EventCalendarBody. Forwards all native <div> props, and every part accepts className, merged last.
| Prop | Type | Default | Description |
|---|---|---|---|
| events* | CalendarEvent[] | - | { id, title, start, end, allDay?, color? }. An all-day event's end is its last day, inclusive. |
| onEventChange | (event: CalendarEvent) => void | - | Called with the updated event when a drag, a resize or a key moves it. Write it back into events. |
| onEventClick | (event: CalendarEvent) => void | - | Called on a click, or Enter on a focused event. Never after a drag. |
| onSlotClick | (slot: CalendarSlot) => void | - | Called when empty space is clicked: a snapMinutes × 2 slot in the time grid, a whole day in the month grid. |
| view | "month" | "week" | "day" | - | The view on show. Controlled. |
| defaultView | "month" | "week" | "day" | "week" | The view it opens on. Uncontrolled. |
| onViewChange | (view) => void | - | Called when the switch, a day heading or "+N more" changes the view. |
| date | Date | - | Any day inside the period on show. Controlled. |
| defaultDate | Date | today | The day it opens on. Uncontrolled; without it, the calendar opens on the browser's today. |
| onDateChange | (date: Date) => void | - | Called when the reader navigates. |
| views | EventCalendarView[] | ["month", "week", "day"] | The views the switch offers. One view hides the switch. |
| weekStartsOn | 0–6 | 1 | The first day of the week, 0 for Sunday. |
| locale | string | the browser's | BCP 47 locale for every label. |
| editable | boolean | true | Events can be dragged, resized and moved with the keyboard. |
| snapMinutes | number | 15 | Drags, resizes and keys move in steps of this many minutes. |
| hourHeight | number | 48 | The height of one hour in the time grid, in px. |
| startHour | number | 0 | The first hour the time grid shows. |
| endHour | number | 24 | The hour the time grid ends at. |
| scrollToHour | number | 8 | The hour the time grid opens scrolled to. |
| maxEventsPerDay | number | 3 | Rows a month cell fills before the last one becomes "+N more". |
| Data attribute | Description |
|---|---|
| data-view | month, week or day. |
EventCalendarHeader
The bar above the grid. With no children: title, navigation and the view switch. Forwards all native <div> props.
EventCalendarTitle
An <h2> naming the period, the year in the muted ink, announced politely when it changes. Forwards all native <h2> props.
EventCalendarNav
Today, previous and next: an outline Button and two ghost icon buttons. Forwards all native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| todayLabel | string | "Today" | The label of the button that returns to today. |
EventCalendarViewSwitch
A segmented ToggleGroup over the root's views; renders nothing with one view. Forwards ToggleGroup props.
| Prop | Type | Default | Description |
|---|---|---|---|
| labels | Partial<Record<EventCalendarView, string>> | - | The label of each view. |
EventCalendarBody
The grid for the current view. A new period fades and rises in once the reader navigates, never on load. Forwards all native <div> props.