CustomizationApplies toSuperScheduler Pro
React render slots and hover cards
Import SuperSchedulerComponent from super-scheduler/react-render instead of the package root, then pass renderEvent, renderRowHeader, renderTimeHeader, renderCorner, renderCell or renderArea; each returns the React content of one kind of slot. The HTML or text fallback paints first and React replaces it in short idle batches, so scrolling never waits for React. Add eventHover for hover cards that users can pin.
SuperScheduler paints its grid with its own DOM code, which is what keeps scrolling smooth with thousands of rows and events. When the content of an event or a header should come from your React components (your design system, icons, avatars, formatted values), the super-scheduler/react-render entry mounts React content into the scheduler's slots without giving React control of the grid.
React render slots and hover cards need SuperScheduler Pro.
Switch to the React-render component
super-scheduler/react-render exports its own SuperSchedulerComponent. It accepts every prop of the main component, exposes the same ref.current.control and controlRef, and adds the render* props, eventHover, renderOptions and the onBefore*DomAdd / onBefore*DomRemove handlers.
The component from the package root accepts these props too, but only warns once (needs the component from "super-scheduler/react-render") and renders nothing from them. Keeping the React machinery in its own entry means pages that do not use it do not load it.
The slots
| Prop | Arguments | Replaces |
|---|---|---|
renderEvent | control, e, data, row, width, lod | The content of an event box |
renderRowHeader | control, row, column | The content of a row header cell (column is the column index, 0 without columns) |
renderTimeHeader | control, header (start, end, level) | The content of a time header cell |
renderCorner | control | The top-left corner |
renderCell | control, cell | The content of a grid cell |
renderArea | control, area, source | An area declared with render: true |
The engine keeps the parts it owns: the event box and its position, the duration bar, resize handles, ordinary areas, the tree toggle and the grid lines. A slot is the content inside.
Render event content
import { memo, useMemo } from 'react'
import { SuperScheduler } from 'super-scheduler'
import type { SchedulerProps } from 'super-scheduler'
import { SuperSchedulerComponent } from 'super-scheduler/react-render'
import 'super-scheduler/styles.css'
type Campaign = { client: string; channel: 'social' | 'print' | 'video'; progress: number }
const CHANNEL_LABEL: Record<Campaign['channel'], string> = {
social: 'Social',
print: 'Print',
video: 'Video',
}
const CampaignContent = memo(function CampaignContent(props: {
title: string
campaign: Campaign
compact: boolean
}) {
const { title, campaign, compact } = props
if (compact) return <strong className="campaign__title">{title}</strong>
return (
<span className="campaign">
<strong className="campaign__title">{title}</strong>
<span className="campaign__meta">
{campaign.client} · {CHANNEL_LABEL[campaign.channel]} ·{' '}
{Math.round(campaign.progress * 100)}%
</span>
</span>
)
})
// Module-level functions keep their identity: a new function re-renders every slot.
const renderEvent: NonNullable<SchedulerProps['renderEvent']> = ({ e, data, width, lod }) => {
// `data` is the event after onBeforeEventRender; custom fields need a cast.
const campaign = data as SuperScheduler.EventRenderData<Campaign>
// `width` comes in 8 px steps and changes only when a gesture ends.
return (
<CampaignContent title={e.text()} campaign={campaign} compact={width < 160 || lod !== 'full'} />
)
}
// The HTML fallback paints first and stays if the React content fails.
const onBeforeEventRender: NonNullable<SchedulerProps['onBeforeEventRender']> = (args) => {
args.data.html = SuperScheduler.Util.escapeHtml(args.data.text)
}
export function CampaignBoard(props: {
resources: SuperScheduler.ResourceData[]
campaigns: SuperScheduler.EventData<Campaign>[]
}) {
const events = useMemo(() => props.campaigns.slice(), [props.campaigns])
return (
<SuperSchedulerComponent
startDate="2026-10-01"
days={61}
scale="Day"
cellWidth={36}
eventHeight={44}
resources={props.resources}
events={events}
onBeforeEventRender={onBeforeEventRender}
renderEvent={renderEvent}
/>
)
}You should see each campaign with its client, channel and progress, and only the title when the event is narrower than 160 px or zoomed out.
The arguments, in detail:
eis the event wrapper:e.id(),e.text(),e.start(),e.end(), ande.datafor the stored object.datais the event asonBeforeEventRenderleft it, withstartandendasSuperScheduler.Datevalues. Custom fields need a cast, as in the snippet.widthis the rendered width in 8 px steps, updated when a gesture ends rather than on every frame.lodis the level of detail ('full','compact'or'overview') when the content was rendered.
Headers, corner, cells and areas
import { useMemo } from 'react'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import { SuperSchedulerComponent } from 'super-scheduler/react-render'
const TIME_HEADERS: SuperScheduler.TimeHeaderData[] = [{ groupBy: 'Month' }, { groupBy: 'Day' }]
// Scheduler dates are civil values: format their native Date in UTC to keep the wall clock.
const WEEKDAY = new Intl.DateTimeFormat('en-US', { weekday: 'short', timeZone: 'UTC' })
// Every slot of a kind gets the function's result: return content for each case
// (a null result leaves that slot empty rather than showing the fallback).
const SLOTS: SchedulerProps = {
renderRowHeader: ({ row }) => {
const role = typeof row.data.role === 'string' ? row.data.role : ''
return (
<span className="person">
<span className="person__initials" aria-hidden="true">
{row.name.slice(0, 1)}
</span>
<span className="person__name">{row.name}</span>
{role !== '' && <span className="person__role">{role}</span>}
</span>
)
},
renderTimeHeader: ({ header }) =>
header.level === 0 ? (
<span>{header.start.toString('MMMM yyyy')}</span>
) : (
<span className="day">
<small>{WEEKDAY.format(header.start.toDate())}</small> {header.start.toString('d')}
</span>
),
renderCorner: () => <span className="corner">Team</span>,
// Only areas declared with `render: true` reach renderArea.
renderArea: ({ area }) =>
area.id === 'approval' ? <span className="badge">Needs approval</span> : null,
onBeforeEventRender: (args) => {
if (args.data.status === 'draft') {
args.data.areas = [
{ id: 'approval', render: true, right: 4, top: 4, width: 96, height: 16, action: 'None' },
]
}
},
}
export function TeamBoard(props: {
resources: SuperScheduler.ResourceData[]
events: SuperScheduler.EventData[]
}) {
const events = useMemo(() => props.events.slice(), [props.events])
return (
<SuperSchedulerComponent
{...SLOTS}
startDate="2026-10-01"
days={31}
scale="Day"
timeHeaders={TIME_HEADERS}
rowHeaderWidth={200}
resources={props.resources}
events={events}
// Keeps React work bounded on large boards (defaults shown).
renderOptions={{ sliceMs: 8 }}
/>
)
}A render function owns every slot of its kind. Return content for each case: a null result leaves that slot empty instead of showing the fallback. renderArea is the exception in practice, because only areas you declared with render: true reach it.
Notes per slot:
- Row headers. The tree toggle stays in place. With
rowHeaderColumns, the function runs once per column and receives its index incolumn. - Time headers.
header.levelis the index intimeHeaders(0 is the top row). Scheduler dates are civil values: to format them withIntl, passdate.toDate()andtimeZone: 'UTC', as the snippet does. - Cells.
renderCellmounts one React root per mounted cell, and none while cells are narrower than 24 px. A view showing 40 rows by 30 days already mounts 1,200 of them: for availability, prices or shading, sethtml,cssClassorbackColorinonBeforeCellRenderinstead. - Areas. Declare the area on the event (or row, cell, header) with
render: trueand its position;sourcetells you which item the area belongs to.
Fallbacks, batching and lifecycle
React content never blocks painting:
- The scheduler paints the HTML or text fallback first: the
htmlortextyour data andonBefore*Renderhooks produce. - When the browser is idle, React content is committed in batches that target
renderOptions.sliceMs(8 ms by default). Each slot hides its fallback once its content is ready. - During scrolling, zooming and dragging, existing content moves with the grid. New slots and renderer changes wait until the gesture ends.
- If a render function throws, that slot keeps its fallback and the error is reported once per slot through the browser's
reportError(a globalerrorevent your error tracking can catch).
Content that scrolls out of view is kept detached so it can come back without re-rendering: up to renderOptions.retain items, by default twice the mounted count with a maximum of 2,000. Local state inside a retained item survives; an evicted item starts over. retain: 0 turns retention off.
Slot content is rendered through portals, so it sees your providers: theme, translations, router, data clients. CSS can target [data-super-scheduler-slot], [data-super-scheduler-slot-ready] and [data-super-scheduler-fallback].
On the server, the component renders an empty <div>; slots appear after the scheduler mounts on the client. See server rendering and prerendering.
Hover cards
eventHover shows a React card next to an event after the pointer rests on it. Without eventHover, no card appears.
| Option | Default | Effect |
|---|---|---|
render(args) | required | Card content; args has control, e, row, anchor (the event's box), pinned and close() |
delay | 350 | Milliseconds the pointer rests before the card opens |
leaveGrace | 180 | Milliseconds before closing after the pointer leaves the event or the card |
placement | 'auto' | 'auto', 'above', 'below', 'start' or 'end' |
pin | false | 'click' or 'dblclick' pins the card so users can interact with it |
glide | true | Moving to another event moves the open card instead of reopening it |
import { useMemo } from 'react'
import type { SchedulerEventHoverOptions, SuperScheduler } from 'super-scheduler'
import { SuperSchedulerComponent } from 'super-scheduler/react-render'
// Module-level: the options object keeps its identity across renders.
const BOOKING_CARD: SchedulerEventHoverOptions = {
delay: 350,
leaveGrace: 180,
placement: 'auto',
// A click pins the card as a non-modal dialog; on touch screens a tap does it.
pin: 'click',
render: ({ e, row, pinned, close }) => (
<article className="booking-card">
<h3>{e.text()}</h3>
<p>{row.name}</p>
<p>
{e.start().toString('d MMM, HH:mm')} to {e.end().toString('d MMM, HH:mm')}
</p>
{pinned && (
<button type="button" onClick={close}>
Close
</button>
)}
</article>
),
}
export function BookingsWithCards(props: {
resources: SuperScheduler.ResourceData[]
events: SuperScheduler.EventData[]
}) {
const events = useMemo(() => props.events.slice(), [props.events])
return (
<SuperSchedulerComponent
startDate="2026-10-01"
days={14}
scale="Day"
resources={props.resources}
events={events}
eventHover={BOOKING_CARD}
/>
)
}How the card behaves:
- An unpinned card is a
role="tooltip"; a pinned card is a non-modalrole="dialog"that takes focus. Escape or a click outside closes a pinned card and returns focus to the event. - Moving the pointer into the card keeps it open. Scrolling, zooming, dragging and selecting hide it at once.
- The card is placed when it opens, flips or shrinks to fit the viewport, and respects reduced motion.
- It lives in
document.bodyand carries the scheduler's theme. Style it with--super-scheduler-hover-padding,-hover-border,-hover-radius,-hover-bg,-hover-color,-hover-shadowand--super-scheduler-z-hover. - Touch screens have no hover: with
pin: 'click', a tap opens a pinned card.
Hover cards are independent of the HTML bubbles (bubble, bubbleHtml). Bubbles cannot host React content; use eventHover for that.
Performance
- Stable functions. Define render functions and option objects at module level, or memoize them. A new function identity re-renders every slot of that kind.
- Cheap renders.
sliceMsis a target for batches, not a limit on your code: one slow render function delays its batch. Do not read layout or measure DOM inside render functions; usewidthandlod. - Memoized components. Wrap slot components in
memoand pass primitive props, as in the event snippet. - Context. A context value that changes often re-renders every slot that reads it. Keep fast-changing state (pointer position, timers) out of contexts that slots consume.
- Cells. Prefer
onBeforeCellRenderstrings torenderCellon large grids. - No per-frame state. Do not set React state from
onScrollor drag handlers; the library does its per-frame work without React renders.
Measure your own content with the React Profiler: the library cannot make an expensive component cheap.
Related
Creative agency resource planningA designer is double-booked. Hand two tasks to a colleague in one drag, check what they unblock, and take it back. Lab instrument bookingBook an instrument and its calibration comes with it. Clear a session out of a service visit, then stretch your run.
- Themes, tokens, Tailwind and dark mode for styling slot content with the scheduler's tokens.
- Performance and virtualization for the rendering model behind slots.
Related examples
- Studio NorthCreative agency resource planningA designer is double-booked. Hand two tasks to a colleague in one drag, check what they unblock, and take it back.
- BenchlabLab instrument bookingBook an instrument and its calibration comes with it. Clear a session out of a service visit, then stretch your run.