# React-Render-Slots und Hover-Karten

> Ereignisse, Zeilen, Köpfe, Zellen und Areas mit super-scheduler/react-render als React-Komponenten rendern, Hover-Karten ergänzen und Scrollen flüssig halten.

Source: https://superscheduler.org/de/docs/react-render-slots/
Reviewed: 2026-10-07

Importieren Sie SuperSchedulerComponent aus super-scheduler/react-render statt aus dem Paket-Root und übergeben Sie dann renderEvent, renderRowHeader, renderTimeHeader, renderCorner, renderCell oder renderArea; jede dieser Funktionen liefert den React-Inhalt einer Art von Slot. Der HTML- oder Text-Fallback wird zuerst gezeichnet, und React ersetzt ihn in kurzen Batches, wenn der Browser untätig ist; das Scrollen wartet also nie auf React. Ergänzen Sie eventHover für Hover-Karten, die Nutzer anheften können.

SuperScheduler zeichnet sein Raster mit eigenem DOM-Code; genau das hält das Scrollen bei Tausenden von Zeilen und Ereignissen flüssig. Wenn der Inhalt eines Ereignisses oder eines Kopfs aus Ihren React-Komponenten kommen soll (Ihr Designsystem, Icons, Avatare, formatierte Werte), bindet der Einstiegspunkt `super-scheduler/react-render` React-Inhalte in die Slots des Planers ein, ohne React die Kontrolle über das Raster zu geben.

React-Render-Slots und Hover-Karten erfordern SuperScheduler Pro.

## Zur React-Render-Komponente wechseln
`super-scheduler/react-render` exportiert ein eigenes `SuperSchedulerComponent`. Es akzeptiert jede Prop der Hauptkomponente, stellt dieselben `ref.current.control` und `controlRef` bereit und ergänzt die `render*`-Props, `eventHover`, `renderOptions` sowie die Handler `onBefore*DomAdd` / `onBefore*DomRemove`.

Die Komponente aus dem Paket-Root akzeptiert diese Props ebenfalls, warnt aber nur einmal (`needs the component from "super-scheduler/react-render"`) und rendert daraus nichts. Weil die React-Mechanik in einem eigenen Einstiegspunkt liegt, laden Seiten, die sie nicht verwenden, sie auch nicht.

## Die Slots
| Prop | Argumente | Ersetzt |
|---|---|---|
| `renderEvent` | `control`, `e`, `data`, `row`, `width`, `lod` | Den Inhalt eines Ereigniskastens |
| `renderRowHeader` | `control`, `row`, `column` | Den Inhalt einer Zelle im Zeilenkopf (`column` ist der Spaltenindex, 0 ohne Spalten) |
| `renderTimeHeader` | `control`, `header` (`start`, `end`, `level`) | Den Inhalt einer Zelle im Zeitkopf |
| `renderCorner` | `control` | Die linke obere Ecke |
| `renderCell` | `control`, `cell` | Den Inhalt einer Rasterzelle |
| `renderArea` | `control`, `area`, `source` | Eine Area, die mit `render: true` deklariert ist |

Die Engine behält die Teile, die ihr gehören: den Ereigniskasten und seine Position, den Dauerbalken, die Griffe zum Ändern der Dauer, gewöhnliche Areas, den Umschalter des Baums und die Rasterlinien. Ein Slot ist der Inhalt darin.

## Inhalte von Ereignissen rendern
```tsx
// src/CampaignBoard.tsx
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}
    />
  )
}
```
Sie sollten jede Kampagne mit Kunde, Kanal und Fortschritt sehen und nur den Titel, wenn das Ereignis schmaler als 160 px oder herausgezoomt ist.

Die Argumente im Detail:

- `e` ist der Ereignis-Wrapper: `e.id()`, `e.text()`, `e.start()`, `e.end()` und `e.data` für das gespeicherte Objekt.
- `data` ist das Ereignis in dem Zustand, in dem `onBeforeEventRender` es hinterlassen hat, mit `start` und `end` als `SuperScheduler.Date`-Werten. Eigene Felder brauchen einen Cast, wie im Snippet.
- `width` ist die gerenderte Breite in Schritten von 8 px, aktualisiert am Ende einer Geste statt in jedem Frame.
- `lod` ist die Detailstufe (`'full'`, `'compact'` oder `'overview'`) zum Zeitpunkt des Renderns.

> **Behavior:**
> React-Inhalte sind rein visuell. Der barrierefreie Name des Ereignisses stammt weiterhin aus `ariaLabel`, `text` oder `html` (siehe [Tastatur und Barrierefreiheit](https://superscheduler.org/de/docs/keyboard-accessibility-touch/#focus-model)); halten Sie `text` also aussagekräftig. Ein Zeigerdruck innerhalb eines Ereignisses startet dessen eigene Klick- und Ziehverarbeitung: Halten Sie Buttons und Links aus dem Inhalt von Ereignissen heraus und legen Sie Aktionen in eine Hover-Karte, ein Kontextmenü oder ein Detailpanel.

## Köpfe, Ecke, Zellen und Areas
```tsx
// src/TeamBoard.tsx
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 }}
    />
  )
}
```
Eine Render-Funktion besitzt jeden Slot ihrer Art. Liefern Sie für jeden Fall Inhalt: Ein Ergebnis `null` lässt den Slot leer, statt den Fallback zu zeigen. `renderArea` ist in der Praxis die Ausnahme, weil nur Areas, die Sie mit `render: true` deklariert haben, die Funktion erreichen.

Hinweise pro Slot:

- **Zeilenköpfe.** Der Umschalter des Baums bleibt an seinem Platz. Mit `rowHeaderColumns` läuft die Funktion einmal pro Spalte und erhält deren Index in `column`.
- **Zeitköpfe.** `header.level` ist der Index in `timeHeaders` (0 ist die oberste Zeile). Datumsangaben des Planers sind bürgerliche Zeitwerte: Um sie mit `Intl` zu formatieren, übergeben Sie `date.toDate()` und `timeZone: 'UTC'`, wie im Snippet.
- **Zellen.** `renderCell` mountet einen React-Root pro gemounteter Zelle und keinen, solange Zellen schmaler als 24 px sind. Eine Ansicht mit 40 Zeilen mal 30 Tagen mountet bereits 1.200 davon: Für Verfügbarkeit, Preise oder Schattierung setzen Sie stattdessen `html`, `cssClass` oder `backColor` in `onBeforeCellRender`.
- **Areas.** Deklarieren Sie die Area am Ereignis (oder an Zeile, Zelle, Kopf) mit `render: true` und ihrer Position; `source` sagt Ihnen, zu welchem Element die Area gehört.

## Fallbacks, Batches und Lebenszyklus
React-Inhalte blockieren nie das Zeichnen:

1. Der Planer zeichnet zuerst den HTML- oder Text-Fallback: das `html` oder den `text`, den Ihre Daten und die `onBefore*Render`-Hooks liefern.
2. Wenn der Browser untätig ist, werden React-Inhalte in Batches übernommen, die `renderOptions.sliceMs` anpeilen (standardmäßig 8 ms). Jeder Slot blendet seinen Fallback aus, sobald sein Inhalt bereit ist.
3. Während Scrollen, Zoomen und Ziehen bewegen sich vorhandene Inhalte mit dem Raster. Neue Slots und Änderungen am Renderer warten, bis die Geste endet.
4. Wirft eine Render-Funktion einen Fehler, behält dieser Slot seinen Fallback, und der Fehler wird einmal pro Slot über `reportError` des Browsers gemeldet (ein globales `error`-Event, das Ihr Error-Tracking abfangen kann).

Inhalte, die aus dem sichtbaren Bereich scrollen, werden abgelöst aufbewahrt, damit sie ohne erneutes Rendern zurückkommen können: bis zu `renderOptions.retain` Elemente, standardmäßig das Doppelte der gemounteten Anzahl, höchstens 2.000. Der lokale State eines aufbewahrten Elements bleibt erhalten; ein verdrängtes Element beginnt von vorn. `retain: 0` schaltet die Aufbewahrung ab.

Slot-Inhalte werden über Portale gerendert und sehen daher Ihre Provider: Theme, Übersetzungen, Router, Daten-Clients. CSS kann `[data-super-scheduler-slot]`, `[data-super-scheduler-slot-ready]` und `[data-super-scheduler-fallback]` ansprechen.

Auf dem Server rendert die Komponente ein leeres `<div>`; Slots erscheinen, nachdem der Planer im Client gemountet ist. Siehe [Server-Rendering und Prerendering](https://superscheduler.org/de/docs/ssr-prerender/).

> **Tip:**
> Code, der aus Callback-basierten Planern portiert wurde, kann `onBeforeEventDomAdd` und seine Geschwister (Zelle, Zeilenkopf, Zeitkopf, Ecke) verwenden: Setzen Sie `args.element` auf einen DOM-Knoten oder ein React-Element, und der passende `DomRemove`-Handler erhält dasselbe Element. Gibt es beides, gewinnt die `render*`-Prop und protokolliert eine Warnung.

## Hover-Karten
`eventHover` zeigt neben einem Ereignis eine React-Karte, nachdem der Zeiger darauf verweilt hat. Ohne `eventHover` erscheint keine Karte.

| Option | Standard | Wirkung |
|---|---|---|
| `render(args)` | Pflicht | Inhalt der Karte; `args` hat `control`, `e`, `row`, `anchor` (den Kasten des Ereignisses), `pinned` und `close()` |
| `delay` | `350` | Millisekunden, die der Zeiger verweilt, bevor sich die Karte öffnet |
| `leaveGrace` | `180` | Millisekunden bis zum Schließen, nachdem der Zeiger das Ereignis oder die Karte verlassen hat |
| `placement` | `'auto'` | `'auto'`, `'above'`, `'below'`, `'start'` oder `'end'` |
| `pin` | `false` | `'click'` oder `'dblclick'` heftet die Karte an, damit Nutzer mit ihr interagieren können |
| `glide` | `true` | Wechselt der Zeiger zu einem anderen Ereignis, wandert die offene Karte mit, statt sich neu zu öffnen |

```tsx
// src/BookingsWithCards.tsx
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}
    />
  )
}
```
So verhält sich die Karte:

- Eine nicht angeheftete Karte ist ein `role="tooltip"`; eine angeheftete Karte ist ein nicht modaler `role="dialog"`, der den Fokus übernimmt. Esc oder ein Klick außerhalb schließt eine angeheftete Karte und gibt den Fokus an das Ereignis zurück.
- Bewegt sich der Zeiger in die Karte, bleibt sie offen. Scrollen, Zoomen, Ziehen und Auswählen blenden sie sofort aus.
- Die Karte wird beim Öffnen platziert, klappt um oder schrumpft, damit sie in den Viewport passt, und berücksichtigt reduzierte Bewegung.
- Sie lebt in `document.body` und trägt das Theme des Planers. Gestalten Sie sie mit `--super-scheduler-hover-padding`, `-hover-border`, `-hover-radius`, `-hover-bg`, `-hover-color`, `-hover-shadow` und `--super-scheduler-z-hover`.
- Touchscreens kennen kein Hover: Mit `pin: 'click'` öffnet ein Tippen eine angeheftete Karte.

Hover-Karten sind unabhängig von den HTML-Blasen (`bubble`, `bubbleHtml`). Blasen können keine React-Inhalte aufnehmen; verwenden Sie dafür `eventHover`.

## Performance
- **Stabile Funktionen.** Definieren Sie Render-Funktionen und Optionsobjekte auf Modulebene oder memoisieren Sie sie. Eine neue Funktionsidentität rendert jeden Slot dieser Art neu.
- **Günstige Renderings.** `sliceMs` ist ein Zielwert für Batches, keine Begrenzung Ihres Codes: Eine einzige langsame Render-Funktion verzögert ihren Batch. Lesen Sie in Render-Funktionen kein Layout und messen Sie kein DOM; verwenden Sie `width` und `lod`.
- **Memoisierte Komponenten.** Umschließen Sie Slot-Komponenten mit `memo` und übergeben Sie primitive Props, wie im Ereignis-Snippet.
- **Context.** Ein Context-Wert, der sich oft ändert, rendert jeden Slot neu, der ihn liest. Halten Sie schnell wechselnden State (Zeigerposition, Timer) aus Contexts heraus, die Slots konsumieren.
- **Zellen.** Bevorzugen Sie bei großen Rastern Strings aus `onBeforeCellRender` gegenüber `renderCell`.
- **Kein State pro Frame.** Setzen Sie keinen React-State aus `onScroll` oder Zieh-Handlern; die Bibliothek erledigt ihre Arbeit pro Frame ohne React-Renderings.

Messen Sie Ihre eigenen Inhalte mit dem React Profiler: Die Bibliothek kann eine teure Komponente nicht günstig machen.

## Siehe auch
→ https://superscheduler.org/de/examples/agency-campaigns/
→ https://superscheduler.org/de/examples/lab-instruments/
- [Themes, Tokens, Tailwind und Dark Mode](https://superscheduler.org/de/docs/theming/), um Slot-Inhalte mit den Tokens des Planers zu gestalten.
- [Performance und Virtualisierung](https://superscheduler.org/de/docs/performance-virtualization/) zum Rendering-Modell hinter den Slots.
