Calendar

Compose a month, week, or day calendar with editable events and timezone-aware dates.

Installation

pnpm add @notion-kit/ui

Import these components from @notion-kit/ui/calendar. For a date picker, use Calendar from @notion-kit/ui/primitives.

Anatomy

import {
  CalendarContent,
  CalendarHeaderToolbar,
  CalendarProvider,
} from "@notion-kit/ui/calendar";

<CalendarProvider
  events={events}
  defaultRange="monthly"
  timeZone="America/New_York"
  onCreate={createEvent}
  onEventClick={openEvent}
  onEventChange={changeEvent}
  className="h-[600px]"
>
  <CalendarHeaderToolbar />
  <CalendarContent />
</CalendarProvider>;

The provider owns navigation and temporary gesture state. You own events: apply changes from onCreate and onEventChange to save them. Ignoring a change leaves the original event in place. Omitting a write callback disables its operation. readOnly disables creation, moving, resizing, and conversion; navigation and event opening remain available.

Examples

Week

The week view separates all-day events from timed events. Overlapping events appear in adjacent columns, and events that cross midnight occupy each day they cover. This example uses UTC and saves creation, movement, resizing, and conversion in local React state.

Day with controlled navigation

Control range and anchorDate to keep the selected period and viewed date in your application. Apply each navigation callback to update the calendar.

Read-only events

Set readOnly to disable event mutations. Users can still change the range, navigate dates, and open events through onEventClick.

Custom event cards

Pass renderEvent to replace the default card. Compose CalendarEvent.Root, CalendarEvent.Item, and CalendarEvent.Resize to retain event opening, dragging, and resizing. The layout positions each segment and shows resize handles only at the event's actual boundaries.

Event dates

interface CalendarEventData {
  id: string;
  name: string;
  startAt: number;
  endAt: number | null;
  allDay: boolean;
}

Timestamps are milliseconds. endAt is exclusive: an all-day event from September 16 through September 18 ends at local midnight on September 19. Use calendar-day arithmetic in the display timezone; a DST day can have 23 or 25 hours. Invalid events are omitted individually. Invalid timezone names fall back to UTC.

A null end displays one local day for all-day events or one hour for timed events. Moving preserves null; resizing creates a real end. Timed events ending exactly at midnight do not occupy the next day. Existing zero-length or short events keep their values and receive a readable minimum height.

The grid shows local 00:00–24:00. New time slots snap to 15 minutes. A missing DST wall time moves forward to a valid time; a repeated wall time selects the earlier offset. Existing timestamps retain their original offset.

range supports monthly, weekly, and daily. Use range with onRangeChange, or set defaultRange. The same pattern applies to anchorDate, onAnchorDateChange, and defaultAnchorDate.

Changing range retains the viewed date. Previous and Next move one period; month navigation clamps dates to the last day when needed. Today uses the display timezone. Week and Day initially scroll to 08:00.

Click an empty date or all-day slot to request a one-day event. Click a time slot to request a one-hour event. Enter and Space also activate focused slots and events. Drag any visible event segment to move the whole event; only real start and end boundaries show resize handles. Escape cancels a gesture. Changing period or external event values also cancels it.

Drag between the all-day area and the time grid to convert an event. A three-day all-day event dropped at 10:00 ends at 11:00 on its third day. Timed events converted to all-day retain the dates they cover and discard the hidden time portion. A completed gesture sends one callback.

API reference

CalendarProvider

PropTypeDefaultDescription
eventsreadonly CalendarEventData[]RequiredAuthoritative event values.
rangeCalendarRange—Controlled period.
defaultRangeCalendarRangemonthlyInitial uncontrolled period.
onRangeChange(range: CalendarRange) => void—Requests a period change.
anchorDatenumber—Controlled viewed timestamp.
defaultAnchorDatenumberTodayInitial viewed timestamp.
onAnchorDateChange(date: number) => void—Requests a viewed date.
timeZonestringBrowser zoneIANA display timezone.
weekStartsOn0 through 61First weekday, with Sunday as zero.
readOnlybooleanfalseDisables event mutations.
onCreate(value: CalendarEventValue) => void—Receives start, end, and all-day values without an ID.
onEventClick(event: CalendarEventData) => void—Opens an event.
onEventChange(change: CalendarEventChange) => void—Receives ID, full date values, and operation reason.
classNamestring—Sets container size and local styles.

CalendarEventValue contains startAt, endAt, and allDay. CalendarEventChange adds id and reason, where reason is move, resize-start, resize-end, or convert.

Content and controls

ComponentDescription
CalendarContentRenders CalendarRangeHeader and the layout for the current range. Accepts renderEvent and div props.
CalendarMonthRenders the month grid. Accepts renderEvent and div props.
CalendarTimeGridRenders the all-day area and time slots for Week or Day. Accepts renderEvent and div props.
CalendarRangeHeaderRenders weekday or date labels for the current range.
CalendarToolbarContainer for custom controls. Accepts div props.
CalendarJumpToRenders Previous, Today, and Next buttons.

CalendarHeaderToolbar

Combines the period title, range select, and navigation. Also accepts div props; children appear before the range select.

PropTypeDescription
rangeDisabledbooleanDisables the range select while keeping date navigation available.
onRangeChange(range: CalendarRange) => voidOverrides the provider's range-change handler for this toolbar.

CalendarRangeSelect

PropTypeDescription
valueCalendarRangeSelected range. Defaults to the provider's range.
onChange(range: CalendarRange) => voidOverrides the provider's range-change handler.
disabledbooleanDisables selection.

CalendarEvent

PartPropsDescription
Rootevent, segment, div propsProvides event and segment data to the card's compound parts.
ItemButton props except renderOpens or moves the event. children replace the default time and title.
Resizeedge: "start" | "end", button props except renderEdits one actual event boundary when changes are enabled.

renderEvent receives CalendarEventRenderProps: the source event and its visible segment. Pass both to Root; keep Item and Resize inside it.

For events backed by table rows and Date properties, see the Table View calendar layout.