Events and interaction
Dates and identifiers
EventInput.start is required. Provide a stable, unique id whenever events can be updated or
refetched. IDs normalize to strings; reusing one ID makes lookups and replacement ambiguous.
Changing the timezone preserves wall-clock time for offset-free inputs and preserves the instant
for UTC/offset inputs. Callbacks and getEvents() return CalEvent objects, with start/end as
Temporal values, plus startStr/endStr for serialization. Use Temporal methods such as
toString() or add(); these are not Date objects with getTime().
All-day ends are exclusive: September 7–9 inclusive is { start: '2026-09-07', end: '2026-09-10' }.
A date-only event with no end lasts one day. A timed event with no end is a point event;
defaultTimedEventDuration applies when an all-day event is dropped into a time grid, not to every
missing timed end. Give timed events an explicit end when a visible duration matters.
Common optional fields: title, resourceIds, color, backgroundColor, textColor, editable,
startEditable, durationEditable, classNames, and extendedProps. Put application metadata in
extendedProps. Use display: 'background' to shade a range instead of showing an ordinary event.
Load an array or remote source
Use events for a local array. Use eventSources for remote data, as a URL descriptor or a function.
A function must return a Promise<EventInput[]> or call its success/failure callbacks; a bare
synchronous array is not its return contract. When sources are configured, their results replace
the store rather than append to the static events array; use one input authority.
For a simple JSON feed, use eventSources: [{ url: '/api/events' }]. The endpoint returns a JSON
array, not an { events: [...] } envelope. The built-in loader supplies start, end, and
timeZone; GET/HEAD append query parameters, other methods send URL-encoded form data. URL sources
accept method and extraParams. For custom headers, credentials, validation, or an error banner,
use a function source and your own fetch implementation.
lazyFetching defaults to true: a previously covered range can be reused. refetchEvents()
invalidates the cache. Replacing a source invalidates its cache even for the same dates.
fetchPadding (default false) extends the requested range by one view duration before and after
the active range, so with lazy fetching the adjacent period is served from the cached fetch instead
of a new request; with lazyFetching: false every navigation fetches the padded range. Remote
refreshes retain current events until all sources settle, then apply one snapshot; stable IDs
keep existing elements mounted. Failed sources log errors and contribute no events to a partially successful refresh. If all
sources fail, the current store is retained. Use refetchEvents() to retry; surface errors in your
function source when users need feedback.
Add, replace, and remove
The imperative API mutates the calendar's current store. It does not write to a server or durable storage. API edits survive ordinary navigation, but replacement arrays and subsequent remote fetches are authoritative. When using remote data, save changes before refetching.
addEvent returns a CalEvent; updateEvent returns it or null when the ID is missing.
getEvents() returns the currently loaded store, not every event on your server.
Choose one owner for updates. In React, either keep events in application state and replace it
intentionally, or use the instance API and keep the input array stable. Creating a new events
array on each render can overwrite imperative changes. See React data ownership.
Recurring events
Add the Recurrence plugin and give an event a recurrence rule. The calendar expands the rule
into occurrences for the visible range each time the range changes, so a series with no end costs
only what is on screen and nothing is pre-expanded in your data.
The event start is the anchor: the first occurrence when it matches the rule, and the wall clock
of every occurrence. end sets the duration of every occurrence. rrule accepts a bare
FREQ=… value, an RRULE: line, or several lines with DTSTART (its TZID sets the zone; the
value is ignored in favour of start) and EXDATE. Supported parts are FREQ (DAILY, WEEKLY,
MONTHLY, YEARLY), INTERVAL, COUNT, UNTIL, BYDAY (with ordinals such as 2TU and
-1FR), BYMONTHDAY, BYMONTH, BYSETPOS, and WKST. Sub-daily frequencies, BYHOUR and
finer, BYWEEKNO, BYYEARDAY, and RDATE throw a ZCalParseError rather than silently changing
the series. The object form covers the same ground:
Rules follow RFC 5545: a monthly rule anchored on the 31st skips months without a 31st, and a yearly
rule anchored on February 29 skips non-leap years. A date-only until includes the whole day.
count counts generated occurrences, so removing one with exdates does not pull a later one in.
Time zones and daylight saving
Timed occurrences are computed as wall clock in the rule zone, then shown in the calendar zone. A
09:00 standup stays at 09:00 through both transitions, its duration stays one civil hour, and an
anchor inside a spring-forward gap moves to the next valid time on that day only. Set
recurrence.timeZone when the series belongs to a fixed zone (a Tokyo meeting viewed from New York);
without it, floating inputs re-anchor in the new zone when the calendar timeZone changes, and
instant inputs keep their instants.
Occurrence identity
Every occurrence is a CalEvent whose id is the series id and whose occurrence says which one it
is, in every callback (eventClick, eventDrop, eventContent, tooltips) and in
getEventOccurrences():
The key is the original start as wall clock in the rule zone (2026-09-16T09:00:00, or
2026-09-16 for all-day series). It is stable across calendar zone changes and DST, indexes
overrides, and appears on event elements as data-cx-occurrence, so
getEventElements('standup', key) returns the elements of one occurrence.
getEvents() and getEventById(seriesId) return each series once, as its master at the anchor;
getEventOccurrences(range?) returns what the views show.
An override is a partial event input applied to one occurrence: move it, retitle it, recolor it.
Without its own end it keeps the series duration. A moved occurrence renders where it moved and
nowhere else, even outside the weeks its rule generates:
Editing occurrences
Dragging or resizing an occurrence never decides scope: it writes an override for that occurrence on
the series (the "this occurrence" result), so the view is correct immediately and revert() puts
back the override that existed before the gesture. eventDrop.event.occurrence tells you which
occurrence moved. The app then asks the user for scope and applies pure helpers to its own series
input; the calendar exports them from z-cal and z-cal/recurrence:
'this'returns the series with the override merged over any existing one for that key, which is what the drop already applied.'following'returnsupdate, the series truncated before the occurrence (untilset to the previous occurrence, orcountreduced), andadd, a new series starting at the moved occurrence. Splitting at the first occurrence returnsupdate: null: remove the series and add the new one.'all'shifts the whole series by the drag delta: the anchor,until, and every exception move with it, and the weekday or day-of-month pattern moves so the shape is preserved (a Monday/Wednesday series dragged to Tuesday becomes Tuesday/Thursday).
Exceptions after a split point are shifted and re-keyed onto the new series. The helpers keep the
input syntax: a series written as rrule text comes back as text, an object rule as an object, and
endpoints follow the series' floating or instant style. overrideOccurrence, splitSeries, and
shiftSeries are the individual helpers. React owners map their events state the same way; see
React data ownership.
Server-expanded series
If your server already expands series, keep doing so and tag each instance with recurringEventId
and originalStart (the original occurrence start; defaults to start). Occurrences then report
occurrence.seriesId, key, and overridden, with index undefined, and commit like plain events.
The Recurrence plugin is not needed for this mode. Without the plugin, a recurrence rule renders
its anchor once and warns.
Select to create
Register Interaction, enable selectable, and handle select. Selection alone does not create
an event or open a built-in editor. This compact example uses a browser prompt; replace it with
your application's accessible form and save flow. A resource selection includes info.resource.
For a custom form that stays open, unselectAuto: false retains the highlight until you call
unselect() on save or cancel. Alternatively, use unselectCancel with a selector for your editor.
selectConstraint must accept the current candidate for a selection to complete. A rejected
candidate clears the preview and does not fire select; moving back to an allowed range can
restore it. The constraint runs again on pointer release, so a rule changed during the gesture
can reject the selection. Disabling selectable before release also cancels it.
With keyboardNavigation, Shift+Arrow previews a selection from the focused cell or slot, Enter
completes it, and Escape cancels it; select then carries the KeyboardEvent as jsEvent. See
keyboard navigation.
The playground includes a form with focus management, title validation, cancel, and Escape dismissal. Its events are in memory and reset on reload.
Drag, resize, and persist
Minimum timed event height
eventMinHeight sets a minimum height in CSS pixels for timed foreground events and their
drag/resize previews in time-grid and resource time-grid views. It defaults to 0 (duration-based
sizing). For example, { slotDuration: '00:15', slotHeight: 14, eventMinHeight: 28 } keeps a
15-minute event readable at 28px. The value must be finite and nonnegative.
Timed events adapt their content to their height. Under two lines of text the title and time share
one line, and under one line the text shrinks, so 30 and 15 minute events at the default
slotHeight stay readable. Custom eventContent keeps this behavior when it uses the
cx-event-title and cx-event-time class names.
Expanded boxes participate in collision packing. Near slotMaxTime, boxes shift upward to stay
inside the grid; a minimum taller than the full visible range is capped to that range. Actual event
dates and callback deltas remain unchanged by expansion. Background events, selection ranges, and
pointer helpers retain exact time geometry. Colliding boxes use the adaptive overlap layout
described below. This option does not affect month, list, all-day, or horizontal timeline events.
Overlapping timed events
Time-grid and resource time-grid views keep every foreground event on the grid. Groups requiring
two or three lanes use staggered overlapping cards. Groups requiring four or more lanes use narrow
side-by-side strips so every event remains exposed and independently selectable. Packing follows
start time (using eventOrder to break equal-start ties), reuses lanes as events end, and expands
cards across adjacent lanes that stay free for their entire rendered height. This keeps chains of
overlaps from wasting columns and gives quieter portions of a cluster more space. No timed events
are replaced by overflow controls. Actual start/end times and drag/resize geometry stay unchanged.
Hover an event for 200ms or focus it with the keyboard to see its full title, date, time range, and
resource (when applicable). The read-only preview uses the browser's top layer and stays within
the viewport without resizing or moving the event cards. It shows both endpoints even when
displayEventEnd is false and wraps long titles. You can move the pointer onto the preview to read
it; Escape dismisses it. Hover previews close on scrolling, resizing, pointer press, or editing.
Keyboard previews follow their focused card when focusing causes the grid to scroll. With
keyboardNavigation, Tab moves from a focused column into that date's cards, and Escape on a card
closes its preview and returns focus to the column.
Click, tap, Enter, and Space retain the normal eventClick behavior and close the preview. A
clicked card stays quiet afterwards, even when the pointer returns to it or it keeps focus, until the
pointer enters another card or focus leaves it. Dragging and resizing operate
on the original card. Touch presses do not trigger a hover preview; tapping opens the application's
normal event details. Background events, selection helpers, and all-day rows do not show previews.
eventTooltip is true by default. Set it to false to turn previews off, or pass a predicate
(info) => boolean that receives the public event, its card el, and the view. Return false
for an event whose details the application already shows, such as one with an open popover, so the
preview and the popover never stack. An open preview closes when the predicate starts refusing it.
eventContent customizes grid cards; eventTooltipContent independently customizes their preview.
The default preview reads the event's title and actual time range. Theme key eventTooltip controls
the preview surface class. The former slotEventOverlap setting has been replaced by the automatic
layout described above.
Customize or replace event previews
eventTooltipContent(info) receives EventTooltipContentInfo: the public event (including
extendedProps), view, original card el, formatted dateText and timeText, and the column's
resource when applicable. timeText includes both endpoints even when displayEventEnd is false.
The callback receives current event data when the preview opens and reruns when the event or formatting changes.
Replacing an event card closes its preview; reopening uses the replacement event’s data.
In vanilla JavaScript, return text, { html }, or { domNodes }, following the other content options.
Static content is also accepted. Returning undefined restores the default content.
React accepts a renderer returning JSX, with the same typed info and React context preserved:
Omit the React renderer to restore the default preview. Returning null renders an empty slot,
consistent with the other React content slots. Content mounts only while a preview is open and
unmounts on dismissal, event removal, disabling, or calendar destruction. The calendar handles
positioning, size changes, hover delay, focus, and dismissal. Default typography applies only to
the default content; custom content inherits the surface styles and can supply its own typography.
Set eventTooltip: false (React: eventTooltip={false}) to disable the built-in preview entirely.
eventMouseEnter, eventMouseLeave, eventClick, and event lifecycle callbacks remain available
for consumers implementing their own UI. The built-in preview retains read-only tooltip behavior;
use a consumer-owned popover for buttons, forms, or different interaction and dismissal rules.
Anchor your own popover to an event
eventClick hands over the clicked element, but that element is one segment of the event and the
calendar re-creates elements when the event moves or the view changes. getEventElements(id) returns
the live elements for an event whenever you need to re-anchor, and pointerDown tells you what a
press hit so dismissal does not depend on reading the calendar's DOM:
Position with the element's rect, or hand the element to a positioning library as its anchor. An
event that spans several rows or days has several elements; pick the one nearest the pointer with
jsEvent or anchor to the first. Presses outside the calendar are yours to observe on document.
consume() swallows one press only (no gesture, no click, no long press for it); whether a press
on another event should dismiss and open, or only dismiss, is the handler's call per press.
Widen a crowded day
In ordinary time-grid views with multiple days, click a date header to widen that day while keeping the neighboring dates visible. Click it again to restore equal widths, or select another date to move the emphasis. The selected header is highlighted and exposes its pressed state to assistive technology. Enter and Space toggle a focused header; Escape on the header restores the week.
Headers, all-day events, timed events, and the now indicator share the same column widths.
Dragging and resizing use the widened grid without changing the displayed date range or scroll
position. Navigating to another range or changing the number of visible days clears the emphasis.
A configured columnWidth is temporarily overridden and restored when the day is collapsed.
Single-day and resource time-grid views do not offer this toggle. Theme key dayFocusButton
customizes the header button; buttonText.expand and buttonText.collapse label its action.
Editing and persistence
Register Interaction and enable editable. Selection is separate and requires selectable.
Editability resolves from event startEditable/durationEditable → event editable → calendar
eventStartEditable/eventDurationEditable → calendar editable (default false).
eventResizableFromStart controls the start resize handle. snapDuration controls snapping and
falls back to the slot duration. Touch interactions use longPressDelay and its event/selection overrides.
List views let you drag events between visible day buckets using wall-clock date shifts. A grip appears on editable rows when hovered or focused; during a drag, the source dims and the destination shows a highlighted header and event preview without shifting the list. List rows do not expose duration resize handles; use a grid or timeline view to resize events.
Pressing Escape during selection, dragging, or resizing cancels the active gesture and removes
its preview. Releasing the pointer afterward does not fire select, eventDrop, or eventResize.
An event gesture that already fired eventDragStart or eventResizeStart fires the matching stop
callback on cancellation, with the Escape KeyboardEvent as jsEvent. Escape also cancels a pending
touch long press or keyboard selection preview. Completed selections remain under your form's control;
clear them with unselect(), which Escape on a focused cell also calls.
eventDrop and eventResize run after a local change. Each includes the previous event and a
revert() function. A gesture applies its change onto whatever revision of the event is current
when the pointer is released. If that event was removed during the gesture, or its dates, all-day
flag, resources, or time zone changed (through updateEvent, a replacement events array, a source
refresh, or timezone reparsing), release cancels the commit, clears the preview, and emits the
matching stop callback without eventDrop or eventResize. Other changes to the event, such as a
title update or a refresh that returns the same dates, are kept: the moved event carries them, and
oldEvent describes the revision the change was applied to. Changes to other events never cancel
a gesture. Release also re-checks editable and the drag/resize constraint against the final
range. A gesture that ends at the original position, or whose every candidate was rejected, emits
only the stop callback and leaves the event untouched.
revert() restores the previous dates and resources only while the gesture's committed dates are
still current, and keeps any other fields changed since. It does nothing after a newer move,
deletion, or timezone change; repeating a successful revert also does nothing. Revert affects the
local store; it does not undo a server write or update an application-owned React events array.
Timed drags shift both endpoints by the same wall-clock offset, so an interval that spans a DST
transition changes length by the offset difference, and changes back when moved away. Two cases
are corrected. An endpoint that lands in a fall-back fold is resolved to whichever of its two
occurrences keeps the elapsed duration. A move that would invert or collapse a positive interval
keeps the shifted start and restores the elapsed duration. For example, in America/New_York,
November 1, 2026 at 01:45-04:00 → 01:15-05:00 lasts 30 minutes; moving it one day yields
November 2 at 01:45 → 02:15, and moving that back yields November 1 at 01:45-05:00 → 02:15-05:00,
still 30 minutes. The preview, constraint payload, and committed event use the corrected range.
delta describes the civil drag offset; persist the resulting event endpoints. Point events remain
zero-duration events.
This recipe requires an application endpoint accepting the serialized fields:
For production, serialize writes per event or use server-side version checks so requests arriving
out of order cannot overwrite newer server data. The local revert guard does not order network
requests. For application-owned React data, reconcile failures in that state as well.
eventDrop and eventResize report committed edits; returning false from these notifications
does not reject the edit. Use the constraint and overlap options below to reject candidates.
Constraints and overlap
dragConstraint, resizeConstraint, and selectConstraint run synchronously on every pointer move
and again on release. Each accepts:
- A predicate of the candidate (
EventDropInfo,EventResizeInfo, orSelectInfo); returnfalseto reject. 'businessHours': the candidate must lie inside the calendar'sbusinessHours, or the target resource's own hours. A timed range must fit one continuous window (windows that pass midnight chain into the next day); an all-day range needs business time on every day it covers. With the option off this rejects every candidate and warns once.- Inline hours: one rule or an array of
{ daysOfWeek, startTime, endTime }, checked the same way without shading the grid. - An event id: the candidate must stay inside that event's range. Unknown ids reject.
Per event, constraint takes the same non-predicate forms and replaces the drag and resize options
for that event; CalEvent.constraint reports it with inline times in seconds from midnight.
eventOverlap (edits) and selectOverlap (selections), both true by default, decide whether the
candidate may intersect the visible events after eventFilter. false blocks on any intersecting
event; a predicate runs per intersecting event and receives (stillEvent, movingEvent) for edits,
with the moving event at its candidate position, or (event) for selections. Per event,
overlap: false blocks whether that event is the still or the moving side. The moving instance
never blocks itself, helpers never block, background events block only with overlap: false, and
events in other resources never block. Zero-duration events block the instant they sit on.
A rejected drag or resize keeps the preview at the event's origin and commits nothing; a rejected
selection drops the preview and does not fire select. Release re-runs the same checks, so an
option changed during the gesture applies to the final range.