Custom content

Content slots render React nodes inside the calendar, and the headless controller lets you replace the built-in toolbar with your own.

CustomContent.tsx
CustomContent.tsx
import {
  Calendar,
  DayGrid,
  TimeGrid,
  useCalendar,
  useCalendarController,
  type CalendarProps,
  type EventInput,
} from 'z-cal/react'
import type { JSX } from 'react'
import { useScheme } from '../DemoFrame'
import { today, todayDate } from '../shared/dates'

const PLUGINS = [DayGrid, TimeGrid]

const EVENTS: EventInput[] = [
  {
    id: 1,
    title: 'Team sync',
    start: today(0, 10),
    end: today(0, 11),
    extendedProps: { location: 'Room A' },
  },
  { id: 2, title: 'Conference', start: todayDate(1), end: todayDate(4) },
  {
    id: 3,
    title: 'Workshop',
    start: today(0, 13),
    end: today(0, 15),
    extendedProps: { location: 'Lab' },
  },
  { id: 4, title: 'Design review', start: today(-1, 16), end: today(-1, 17) },
  { id: 5, title: 'Stand-up', start: today(0, 9), end: today(0, 9, 15) },
  {
    id: 6,
    title: 'Coffee catch-up',
    start: today(1, 11),
    end: today(1, 11, 30),
    extendedProps: { location: 'Cafe' },
  },
]

// Content slots return React nodes. This one renders the default title and time markup itself and
// appends the event's location to the time line. Reusing the core class names keeps the built-in
// layout, including the one-line form for 30 and 15 minute events.
const eventContent: CalendarProps['eventContent'] = (info) => {
  const location = info.event.extendedProps['location']
  return (
    <>
      <h4 className="cx-event-title">{String(info.event.title)}</h4>
      {info.timeText && (
        <time className="cx-event-time">
          {info.timeText}
          {typeof location === 'string' && ` · ${location}`}
        </time>
      )}
    </>
  )
}

export function CustomContentDemo(): JSX.Element {
  const { ref, calendar } = useCalendar()
  const controller = useCalendarController(calendar)

  // Month view only: other views re-create their day cells, so the slot is not requested there.
  const dayCellContent: CalendarProps['dayCellContent'] =
    controller.view === 'dayGridMonth'
      ? (info) => <span className="day-number">{info.date.day}</span>
      : undefined

  return (
    <>
      {/* A headless toolbar: the built-in one is off and these buttons drive the instance. */}
      <div className="headless-toolbar">
        <button type="button" aria-label="Previous" onClick={controller.prev}>
          ‹
        </button>
        <strong>{controller.title}</strong>
        <button type="button" aria-label="Next" onClick={controller.next}>
          ›
        </button>
        <button type="button" onClick={controller.today}>
          Today
        </button>
        <select
          value={controller.view}
          onChange={(e) => calendar?.changeView(e.currentTarget.value)}
          aria-label="View"
        >
          <option value="dayGridMonth">Month</option>
          <option value="timeGridWeek">Week</option>
        </select>
      </div>
      <div className="card">
        <Calendar
          ref={ref}
          plugins={PLUGINS}
          view="dayGridMonth"
          colorScheme={useScheme()}
          headerToolbar={{ start: '', center: '', end: '' }}
          events={EVENTS}
          eventContent={eventContent}
          dayCellContent={dayCellContent}
          height="560px"
        />
      </div>
    </>
  )
}

What to look at

  • eventContent receives the event and its formatted time text. The demo renders the default title and time markup itself and appends the event's location. Reusing the cx-event-title and cx-event-time class names keeps the built-in layout, including the one-line form that 30 and 15 minute events switch to. Look at the Stand-up and Coffee catch-up events in week view.
  • dayCellContent replaces the day number. It is only passed in month view, because the other views re-create their day cells.
  • The toolbar is turned off with empty headerToolbar slots. The buttons above the calendar call prev, next, today, and changeView on the controller. See React.