React

Documentation index · Quickstart

Props and lifecycle

Import from z-cal/react. <Calendar> accepts calendar options directly as props, plus plugins, ref, className, and style. className and style apply to the outer host; height sizes the inner calendar. Content renderer props return React nodes.

The adapter creates the core instance after mounting and patches changed options on subsequent renders. It destroys the instance on unmount. Plugins are fixed for that instance's lifetime; remount with a new React key when the plugin set needs to change.

Data ownership

Keep unchanged arrays/objects in module scope, state, or memoized values. Options are shallow-diffed; new callback identities are supported, while a new event array can replace the calendar's data. Mutating an existing array or event object in place is not a reliable way to trigger updates.

For application-owned data, update React state in response to drag/resize as well as form edits:

import 'z-cal/polyfill'
import { useState } from 'react'
import {
  Calendar,
  TimeGrid,
  Interaction,
  Recurrence,
  editRecurringEvent,
  type CalEvent,
  type EventInput,
} from 'z-cal/react'
import 'z-cal/style.css'

const plugins = [TimeGrid, Interaction, Recurrence]
const initialEvents: EventInput[] = [
  { id: 'one', title: 'Planning', start: '2026-09-07T09:00', end: '2026-09-07T10:00' },
  {
    id: 'standup',
    title: 'Standup',
    start: '2026-09-07T09:30',
    end: '2026-09-07T09:45',
    recurrence: { rrule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR' },
  },
]
export function OwnedEvents() {
  const [events, setEvents] = useState(initialEvents)
  function applyEdit(changed: CalEvent) {
    setEvents((current) => {
      const series = current.find((event) => String(event.id) === changed.id)
      if (series && changed.occurrence) {
        // An occurrence of a series: apply the scope your UI chose ('this' | 'following' | 'all').
        const { update, add } = editRecurringEvent(series, changed, 'this')
        const next = update
          ? current.map((event) => (event === series ? update : event))
          : current.filter((event) => event !== series)
        return add ? [...next, add] : next
      }
      return current.map((event) =>
        String(event.id) === changed.id
          ? {
              ...event,
              start: changed.start,
              end: changed.end,
              allDay: changed.allDay,
              resourceIds: changed.resourceIds,
            }
          : event,
      )
    })
  }
  return (
    <Calendar
      plugins={plugins}
      events={events}
      date="2026-09-07"
      view="timeGridWeek"
      editable
      height="650px"
      eventDrop={({ event }) => applyEdit(event)}
      eventResize={({ event }) => applyEdit(event)}
    />
  )
}

This example keeps data in memory. Add persistence to preserve it after reload. As an alternative, keep the events input stable and use the imperative instance to manage local edits, as in the selection recipe. Avoid mixing replacement props and imperative mutations without deciding which data is authoritative.

Navigation props are also synchronized when their values change; imperative navigation does not write back to your React state. Use datesSet or the controller when your UI needs the current date/view.

Access the instance and build a toolbar

useCalendar() returns { ref, calendar }. Pass ref to the component; calendar is initially null, becomes the live core instance after mounting, and resets to null on unmount. useCalendarController(calendar) subscribes to the title, view, date, available views, and loading state, and exposes navigation commands. Call the hook unconditionally; it accepts null.

import 'z-cal/polyfill'
import { Calendar, DayGrid, useCalendar, useCalendarController } from 'z-cal/react'
import 'z-cal/style.css'

const plugins = [DayGrid]
const hiddenToolbar = { start: '', center: '', end: '' }
export function CustomToolbar() {
  const { ref, calendar } = useCalendar()
  const controller = useCalendarController(calendar)
  return (
    <section>
      <nav aria-label="Calendar navigation">
        <button disabled={!calendar} onClick={controller.prev}>
          Previous
        </button>
        <span aria-live="polite">{controller.title}</span>
        <button disabled={!calendar} onClick={controller.next}>
          Next
        </button>
        <button disabled={!calendar} onClick={controller.today}>
          Today
        </button>
      </nav>
      <Calendar
        ref={ref}
        plugins={plugins}
        view="dayGridMonth"
        height="650px"
        headerToolbar={hiddenToolbar}
      />
    </section>
  )
}

The calendar already announces the title in its own polite live region after navigation (announceNavigation, default true). Keep the aria-live span above only with announceNavigation={false}, or drop it, so screen readers hear the change once. Keyboard options pass through as props; keyboardShortcuts compares map contents, so an inline object is fine.

For a non-reactive handle, a React useRef<CalendarInstance | null>(null) also works. Import CalendarInstance from z-cal/react; the Calendar value exported there is the component. See instance methods for mutation and navigation methods.

Content slots and icons

Renderers receive typed info and replace the corresponding content. Available named slots are eventContent, eventTooltipContent, dayCellContent, moreLinkContent, weekNumberContent, allDayContent, resourceLabelContent, noEventsContent, and titleContent. customButtonContent receives { name } for custom toolbar button labels. Most slot arguments match the core callback info; titleContent receives { title, view } and noEventsContent receives undefined.

Initials are not generated. Add an icon through eventContent, retaining the time and title you want shown. The default title can also be a core content object; contentToText handles that case.

import 'z-cal/polyfill'
import { contentToText } from 'z-cal'
import { Calendar, TimeGrid } from 'z-cal/react'
import 'z-cal/style.css'

const plugins = [TimeGrid]
export function CalendarWithIcons() {
  return (
    <Calendar
      plugins={plugins}
      view="timeGridWeek"
      height="650px"
      eventContent={({ event, timeText }) => (
        <>
          <span className="cx-event-time">
            <svg aria-hidden="true" width="12" height="12" viewBox="0 0 12 12">
              <circle cx="6" cy="6" r="4" fill="currentColor" />
            </svg>{' '}
            {timeText}
          </span>
          <span className="cx-event-title">{contentToText(event.title)}</span>
        </>
      )}
    />
  )
}

The adapter renders slots as portals, preserving React context and event handlers. Use decorative icons with aria-hidden; supply accessible labels for meaningful icon-only controls. Constrain custom content within the event width so long labels do not overlap adjacent columns.

Server rendering

Server rendering produces an empty host element, not a rendered calendar grid. The client mounts the DOM calendar in a layout effect. Do not call the vanilla createCalendar during server rendering. In frameworks with server/client component boundaries, place the calendar in a client component. Browser-only form actions and DOM access belong in effects or event handlers.

For frameworks that recognize the client directive, the module starts like this:

'use client'

import 'z-cal/polyfill'
import { Calendar, DayGrid } from 'z-cal/react'

const plugins = [DayGrid]
export default function ClientCalendar() {
  return <Calendar plugins={plugins} view="dayGridMonth" height="650px" />
}

Import z-cal/style.css in the framework's allowed global stylesheet entry. Import the polyfill in every runtime where your own code uses Temporal; avoid passing non-serializable Temporal instances through server/client prop boundaries. ISO strings are convenient event inputs.