# React Kalender mit Terminen: Monat, Woche, Tag

> Ein React Kalender mit Terminen: Monats-, Wochen-, Tages- und Agenda-Ansicht, Verschieben per Drag, Rollback beim Speichern und Agenda-Liste am Smartphone.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 19. Okt. 2026
- Kategorie: [Komponenten-Guides](https://wingo-ui.com/de/blog/category/components)
- Lesezeit: 10 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/react-calendar-component-with-events
- Englische Fassung: https://wingo-ui.com/blog/react-calendar-component-with-events

## Kurz gesagt

Ein React Kalender mit Terminen braucht Monats-, Wochen-, Tages- und Agenda-Ansicht, überlappende Termine nebeneinander, Verschieben und Ändern der Dauer per Drag samt Rückweg, wenn das Speichern scheitert, und am Smartphone eine Liste statt eines Rasters. Die Calendar View von Wingo UI erledigt das mit einem events-Array und ein paar Callbacks; der kleinere Calendar ist ein Date Picker, der Termine als Punkte unter den Tagen zeigt.

Du brauchst einen React Kalender mit Terminen: die Termine einer Praxis, die Meetings eines Teams, die Woche eines Außendienstlers. Ein Monatsraster mit ein paar farbigen Chips ist schnell gebaut. Dann kommen die echten Daten. Zwei Termine überschneiden sich um 9:30, ein Kongress läuft über vier Tage, jemand zieht einen Besuch auf Freitag und das Speichern schlägt fehl, und auf dem Smartphone besteht die Wochenansicht aus sieben Spalten mit je 50px Breite. Dieser Guide zeigt, was ein Terminkalender können muss, welche Props in der [Calendar View](https://wingo-ui.com/components/calendar-view) von Wingo UI zählen, wie du Termine lädst und speicherst und was du am Smartphone zeigst.

## Was muss ein React Kalender mit Terminen können?

Sechs Dinge:

1. **Monats-, Wochen-, Tages- und Agenda-Ansicht**, mit Zurück, Heute und Weiter und einem Titel, der sagt, welchen Zeitraum du gerade siehst.
2. **Überlappende Termine nebeneinander** im Wochen- und Tagesraster und mehrtägige Termine als ein Balken über die Monatszellen.
3. **Details per Klick**: Uhrzeit, Ort, Teilnehmer, dazu dein eigener Status oder Links.
4. **Verschieben und Ändern der Dauer per Drag**, mit Einrasten auf einen Slot und einem Rückweg für Änderungen, die der Server ablehnt.
5. **Auswahl freier Zeiten**, um einen Termin anzulegen.
6. **Ein Smartphone-Layout** als Liste, denn ein Zeitraster passt dort nicht.

Wenn deine Frage lautet „Welcher Raum, welches Auto oder welche Person ist um 10 Uhr frei?“, brauchst du stattdessen eine Ressourcen-Timeline mit Ressourcen als Zeilen. Das ist eine andere Komponente, und der [Guide zum React Resource Scheduler](https://wingo-ui.com/de/blog/react-resource-scheduler) behandelt sie. Ein Terminkalender beantwortet die Frage „Was steht in meinem Kalender?“.

## Hat shadcn/ui einen Kalender mit Terminen?

Nein. Der [Calendar von shadcn](https://ui.shadcn.com/docs/components/calendar) ist ein Date Picker auf Basis von React DayPicker: einzelne Tage, Zeiträume, Dropdowns für Monat und Jahr. Ein Zeitraster für Woche oder Tag hat er nicht. Wer nach einem shadcn Calendar mit Terminen sucht, meint meist eines von zwei verschiedenen Dingen, und beides gibt es in Wingo UI:

- Ein **Date Picker, der Termine als Punkte zeigt**, damit du belegte Tage siehst, bevor du einen auswählst. Das ist der [Calendar](https://wingo-ui.com/components/calendar). Sein Prop `events` nimmt `{ date, tone?, color?, label? }` und zeichnet bis zu drei Punkte unter einen Tag; Screenreader lesen die Labels vor.
- Ein **React Event Calendar**, der Termine in einem Zeitraster anordnet. Das ist die Calendar View, um die es im Rest dieses Beitrags geht.

## Wie schneiden React-Terminkalender im Vergleich ab?

react-big-calendar, FullCalendar und Mantine Schedule sind die kostenlosen Optionen unter MIT, und die Calendar View von Wingo UI ist die Pro-Option, die du als Quellcode installierst. Diese Namen tauchen auf, wenn du nach „react kalender monat woche tag“ oder einer React Big Calendar Alternative suchst. Versionen und Fakten stammen aus der Doku, dem Repository und npm des jeweiligen Projekts, Stand Oktober 2026.

| Option | Lizenz und Preis | Ansichten | Verschieben per Drag | Hinweise zum Setup |
|---|---|---|---|---|
| [react-big-calendar](https://github.com/jquense/react-big-calendar) 1.20.0 | MIT, kostenlos | month, week, work_week, day, agenda | Addon `withDragAndDrop` mit `onEventDrop` und `onEventResize` | Du musst einen Localizer wählen (Moment, Globalize, date-fns oder Day.js), sein CSS importieren und dem Container eine Höhe geben |
| [FullCalendar](https://fullcalendar.io/docs/react) 7.1.3 | Standard unter MIT und kostenlos; [Premium](https://fullcalendar.io/pricing) ab $480, mit Timeline und vertikalen Ressourcen-Ansichten | Day Grid, Time Grid und Liste (`listDay` bis `listYear`) über Plugins | `editable` mit dem Interaction-Plugin | Mindestens ein View-Plugin in `plugins` übergeben |
| [Mantine Schedule](https://mantine.dev/schedule/schedule/) 9.7.3 | MIT, kostenlos | Tag, Woche, Monat, Jahr, dazu ein Agenda-Button über `withAgenda` | `withEventsDragAndDrop` mit `onEventDrop` | Teil von Mantine; `layout="responsive"` tauscht den Monat gegen eine mobile Monatsansicht |
| [Wingo UI Calendar View](https://wingo-ui.com/components/calendar-view) | Pro, Quellcode gehört dir | Monat, Woche, Tag, Agenda | `editable` mit `onEventChange`, Maus und Stift | Tailwind-Tokens, Light und Dark, ein Smartphone-Layout unter 576px |

FullCalendar ist die bessere Wahl, wenn du vom selben Anbieter auch Ressourcen-Timelines brauchst und bereit bist, für Premium zu zahlen. react-big-calendar ist eine solide kostenlose Standardwahl, wenn du sein CSS gern selbst anpasst. Mantine Schedule lohnt sich, wenn deine App schon auf Mantine läuft. Die Calendar View passt in eine Codebasis mit Tailwind und shadcn-Stil, in der du jede Zeile lesen und ändern willst.

## Welche Props braucht die Calendar View?

Nur eins: `events`. Jeder Termin ist `{ id, title, start: Date, end: Date }`, dazu optional `allDay`, `tone` oder `color`, `location`, `description`, `attendees`, `editable` und `meta`.

```bash
npx wingo-ui@latest add calendar-view
```

```tsx
"use client";

import { useState } from "react";
import { CalendarView, type CalendarEvent } from "@/components/ui/calendar-view";

const appointments: CalendarEvent[] = [
  { id: "p1", title: "Beratung: Emma Carter", start: new Date(2026, 9, 21, 9, 0), end: new Date(2026, 9, 21, 10, 0), tone: "info", location: "Raum 1" },
  { id: "p2", title: "Zahnreinigung: Irene Vance", start: new Date(2026, 9, 21, 9, 30), end: new Date(2026, 9, 21, 10, 30), tone: "success", location: "Raum 2" },
  { id: "p3", title: "Zahnärztekongress, New York", start: new Date(2026, 9, 22), end: new Date(2026, 9, 25), allDay: true },
];

export function Appointments() {
  const [events, setEvents] = useState(appointments);
  return (
    <CalendarView
      events={events}
      defaultDate={new Date(2026, 9, 21)}
      defaultView="week"
      dayStartHour={8}
      dayEndHour={19}
      editable
      onEventChange={(event, change) =>
        setEvents((list) => list.map((e) => (e.id === event.id ? { ...e, ...change } : e)))
      }
      locale="en-US"
    />
  );
}
```

Die beiden Termine am Vormittag überschneiden sich, also legt das Wochenraster sie in zwei Spalten, und der Kongress erscheint als ein Balken über drei Monatszellen (bei ganztägigen Terminen ist ein Ende um Mitternacht Ortszeit exklusiv). Die Props, die du danach am häufigsten änderst:

- `views` und `defaultView`: Der Umschalter zeigt standardmäßig Monat, Woche, Tag und Agenda; bei einer einzigen Ansicht verschwindet er.
- `dayStartHour`, `dayEndHour`, `scrollToHour` (standardmäßig 8) und `hourHeight` (48px, auf Touchscreens mindestens 56px).
- `slotMinutes`: 15, 30 oder 60, das Raster für Verschieben, Ändern der Dauer und Auswahl.
- `hideWeekends`, `showWeekNumbers`, `maxEventsPerDay` (3 Zeilen pro Monatszelle; ein vollerer Tag zeigt zwei Chips und „+N more“).
- `toolbarActions` für einen Button „Neuer Termin“, `labels` für jeden Text, `variant` für `surface`, `outline` oder `plain`.

Die Demo unten öffnet im Wochenraster. Zieh mit Maus oder Stift einen Termin auf eine andere Stunde und dann seine Unterkante nach unten, um ihn zu verlängern.

> Live-Demo (Calendar View): Zieh einen Termin auf eine andere Uhrzeit oder einen anderen Tag: Er rastet im 30-Minuten-Slot ein, und die Zeile unter dem Kalender zeigt die neue Zeit. Zieh an der Unterkante eines Termins, um seine Dauer zu ändern, oder klick auf einen freien Slot, um einen anzulegen. Probier sie unter [Calendar View](https://wingo-ui.com/components/calendar-view) aus und installier sie mit `npx wingo-ui@latest add calendar-view`.

## Wie lade ich die Termine für den sichtbaren Zeitraum?

Steuer `view` und `date` selbst (kontrolliert) und lade neu, sobald sich eins davon ändert. `getVisibleRange(view, date, weekStartsOn, agendaDays)` gibt den ersten und den letzten sichtbaren Tag als Mitternacht Ortszeit zurück, beide inklusive. Zwei Details sind wichtig. Dort steht `weekStartsOn` standardmäßig auf Montag, während der Kalender der Locale folgt (Sonntag bei en-US), also übergib denselben Wert. Und das Ende ist der letzte Tag um Mitternacht, frag also bis zur nächsten Mitternacht ab, sonst fehlen dir die Termine dieses Tags.

```tsx
"use client";

import { useEffect, useState } from "react";
import { addDays } from "date-fns";
import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { CalendarView, getVisibleRange, type CalendarEvent, type CalendarViewMode } from "@/components/ui/calendar-view";

type ApiEvent = Omit<CalendarEvent, "start" | "end"> & { start: string; end: string };
const toEvent = (e: ApiEvent): CalendarEvent => ({ ...e, start: new Date(e.start), end: new Date(e.end) });

export function ClinicCalendar() {
  const [view, setView] = useState<CalendarViewMode>("week");
  const [date, setDate] = useState(() => new Date());
  const [events, setEvents] = useState<CalendarEvent[]>([]);
  const [loading, setLoading] = useState(true);
  const [openId, setOpenId] = useState<string | null>(null);

  useEffect(() => {
    // 0 = Sonntag, derselbe Wochenanfang, den en-US dem Kalender vorgibt
    const { start, end } = getVisibleRange(view, date, 0);
    const controller = new AbortController();
    setLoading(true);
    fetch(`/api/appointments?from=${start.toISOString()}&to=${addDays(end, 1).toISOString()}`, { signal: controller.signal })
      .then((response) => response.json() as Promise<ApiEvent[]>)
      .then((list) => {
        setEvents(list.map(toEvent));
        setLoading(false);
      })
      .catch((error) => {
        if (error.name !== "AbortError") setLoading(false);
      });
    return () => controller.abort();
  }, [view, date]);

  return (
    <CalendarView
      events={events}
      view={view}
      onViewChange={setView}
      date={date}
      onDateChange={setDate}
      loading={loading}
      locale="en-US"
      weekStartsOn={0}
      editable
      onEventChange={(event, change) => {
        // keine Termine in der Vergangenheit
        if (change.start < new Date()) return false;
        const previous = events;
        setEvents((list) => list.map((e) => (e.id === event.id ? { ...e, ...change } : e)));
        fetch(`/api/appointments/${event.id}`, {
          method: "PATCH",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(change),
        })
          .then((response) => {
            if (!response.ok) setEvents(previous);
          })
          .catch(() => setEvents(previous));
      }}
      onSlotSelect={async (slot) => {
        const response = await fetch("/api/appointments", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ title: "Neuer Termin", ...slot }),
        });
        if (!response.ok) return;
        const created = toEvent(await response.json());
        setEvents((list) => [...list, created]);
        setOpenId(created.id);
      }}
      openEventId={openId}
      onOpenEventIdChange={setOpenId}
      toolbarActions={<Button size="sm" leftIcon={<Plus />}>Neuer Termin</Button>}
    />
  );
}
```

`loading` zeigt einen dünnen Balken unter der Toolbar, während die aktuellen Termine sichtbar bleiben. Wenn du eine Woche weiterblätterst, blitzt also kein leeres Raster auf.

## Wie speichere ich das Verschieben per Drag?

Der Kalender zeigt die Änderung in dem Moment, in dem du loslässt, und ruft dann `onEventChange(event, { start, end, allDay })` auf. Gibst du `false` zurück, springt der Termin sofort zurück; so lehnt das Beispiel oben ein Verschieben in die Vergangenheit ab. Für das Speichern auf dem Server aktualisierst du deine Termine, schickst die Anfrage und stellst das vorherige Array wieder her, wenn sie scheitert. Ein neues `events`-Array verwirft die optimistische Kopie des Kalenders, das Zurücksetzen reicht also. Der Snapshot ist nur sicher, solange immer nur ein Speichervorgang läuft; wenn sich Verschiebungen überlappen können, lad nach einem Fehler lieber den Zeitraum neu.

`onSlotSelect` macht freie Zeit auswählbar: Klick auf einen Slot für eine Stunde, oder zieh im Raster nach unten für einen Zeitraum mit einem Platzhalter „New event“. Im Monat klickst du auf einen Tag oder ziehst über mehrere Tage für einen ganztägigen Zeitraum. Öffnest du den neuen Termin direkt nach dem Anlegen über `openEventId`, siehst du sofort seine Details, und dort können deine eigenen Buttons zum Umbenennen oder Löschen sitzen (siehe `meta` unten).

Einzelne Termine sperrst du mit `editable: false`, etwa eine bestätigte Operation, die nur der Empfang verschieben darf.

## Wie funktioniert ein React Event Calendar auf dem Smartphone?

Er wird zur Liste. Ist der Kalender schmaler als 576px, wechseln Monat und Woche zu der `mobileView`, die du festlegst:

- `"agenda"` (Standard): die Tage dieses Monats oder dieser Woche als Sticky-Überschriften über 56px hohen Zeilen mit Uhrzeit, Farbbalken, Titel und Ort. Sie startet beim heutigen Tag.
- `"day"`: der gewählte Tag als Zeitraster; wisch nach links oder rechts, um den Tag zu wechseln.
- `"month"`: ein kompakter Monat mit bis zu drei Punkten pro Tag und den Terminen des gewählten Tags darunter, ebenfalls mit Wischen.

Der Wechsel ist eine Container Query auf dem Kalender selbst. Ein schmales Seitenpanel am Desktop bekommt also ebenfalls das Smartphone-Layout, und beide Layouts stehen im Server-HTML. `layout="mobile"` erzwingt es, zum Beispiel in einem Smartphone-Rahmen. Bei einem Viewport unter 768px öffnen sich die Termindetails als Bottom Sheet statt als Popover, aus den Gründen im [Guide zum React Bottom Sheet](https://wingo-ui.com/de/blog/react-bottom-sheet-for-the-web).

> Live-Demo (Calendar View): Das Smartphone-Layout mit kompaktem Monat: Tipp auf einen Tag mit Punkten, um seine Termine darunter aufzulisten, wisch auf einem Touchscreen seitwärts, um den Monat zu wechseln, und tipp auf einen Termin, um seine Details zu öffnen. Probier sie unter [Calendar View](https://wingo-ui.com/components/calendar-view) aus und installier sie mit `npx wingo-ui@latest add calendar-view`.

Auf Touchscreens ist das Ziehen von Terminen deaktiviert, denn ein Drag, der auf einem Termin beginnt, würde mit dem Scrollen der Seite kollidieren. Wenn deine Nutzer am Smartphone Termine verschieben, gib ihnen einen Button in den Details. Das `meta` jedes Termins erscheint am Ende des Popovers oder Sheets mit den Details:

```tsx
const withActions = (event: CalendarEvent): CalendarEvent => ({
  ...event,
  meta: (
    <Button size="sm" variant="soft" onClick={() => openRescheduleForm(event.id)}>
      Verschieben
    </Button>
  ),
});
```

`openRescheduleForm` ist dein eigenes Formular, zum Beispiel ein [Drawer](https://wingo-ui.com/components/drawer) mit einem Feld für Datum und Uhrzeit. Derselbe Button hilft auch allen, die per Tastatur arbeiten, wie der nächste Abschnitt zeigt.

## Wie mache ich einen Terminkalender barrierefrei?

Gib jeder Aktion einen Weg ohne Ziehen. Das Monatsraster ist ein einziger Tab-Stopp: Pfeiltasten springen um einen Tag oder eine Woche, Home und End gehen an den Anfang oder das Ende der Woche, PageUp und PageDown wechseln den Monat (mit Shift das Jahr), Enter öffnet die Termine des Tags, und Tab geht weiter zu den Terminen an diesem Tag. In Woche, Tag und Agenda sind Termine Buttons in zeitlicher Reihenfolge, vorgelesen als Titel und Zeitspanne. Escape schließt die Details und gibt den Fokus an den Termin zurück. Der Titel in der Toolbar ist eine Live Region, ein Screenreader hört also nach Zurück oder Weiter den neuen Zeitraum.

Ein Termin lässt sich im Raster nicht per Tastatur verschieben (Enter öffnet die Details), ein Grund mehr für einen Button zum Verschieben in `meta`. WCAG 2.2 [2.5.7 Dragging Movements](https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html) verlangt für jede Ziehbewegung eine Alternative mit einem einzelnen Zeiger, und dieser Button ist sie. Bei reduzierter Bewegung ersetzen Überblendungen die Schiebeanimationen zwischen den Zeiträumen.

## Was sind die typischen Fallstricke?

- **Strings statt Dates.** JSON liefert ISO-Strings. Wandle `start` und `end` beim Laden mit `new Date()` um, so wie `toEvent` oben.
- **Ende ganztägiger Termine.** Ein ganztägiger Termin, der um Mitternacht Ortszeit endet, endet am Tag davor. Ein dreitägiger Kongress ab dem 22. endet also um Mitternacht am 25. Liegt das Ende auf oder vor dem Start, gilt der Termin als 30 Minuten lang, mit einer Warnung im Development-Modus.
- **Unterschiedliche Wochenanfänge.** Übergib `getVisibleRange` und dem Kalender dasselbe `weekStartsOn`, sonst wird ein Sonntag bei en-US nicht geladen.
- **Hydration.** Ohne `defaultDate` oder `date` rendert der Server den Tag des ersten Termins, und der Browser springt nach dem Mount auf heute, damit das HTML übereinstimmt. Ein kontrolliertes `date`, das wie im Beispiel zum Laden mit `new Date()` startet, wird auf dem Server und noch einmal im Browser berechnet. Kurz vor Mitternacht oder über Zeitzonen hinweg können die beiden auf verschiedene Tage fallen. Starte mit einem festen Datum oder setz es in einem Effect auf heute.
- **Sehr volle Tage.** Jeder überlappende Termin bekommt eine eigene Spalte. Ein Tag mit Dutzenden paralleler Buchungen zerfällt also in Spalten, die zu schmal zum Lesen sind. Zeig solche Daten in einer [Data Table](https://wingo-ui.com/components/data-table) mit Filtern oder teil sie nach Ressource auf.

## Wie füge ich ihn meinem Projekt hinzu?

Führ `npx wingo-ui@latest add calendar-view` aus (oder `calendar` für den Date Picker). Die CLI kopiert den Quellcode nach `components/ui`, fügt die Teile hinzu, die er nutzt, etwa [Popover](https://wingo-ui.com/components/popover), Avatar und Button, und installiert `@dnd-kit/core`, `date-fns`, `motion` und `lucide-react`. Calendar View und Calendar sind beide Pro-Komponenten ([Preise](https://wingo-ui.com/de/pricing)). Wenn du einen Zeitraum auswählen statt Termine zeigen willst, lies den [Guide zum React Date Range Picker](https://wingo-ui.com/de/blog/react-date-range-picker). Weitere Deep Dives zu einzelnen Komponenten findest du in der [Kategorie Komponenten-Guides](https://wingo-ui.com/de/blog/category/components), und der [Guide zu React-Formularkomponenten](https://wingo-ui.com/de/blog/react-form-components-guide) behandelt die Felder, die du in das Formular zum Verschieben einbaust.

## Komponenten in diesem Artikel

- [Calendar View](https://wingo-ui.com/components/calendar-view) (Pro): A React event calendar for appointments and meetings: month, week, day and agenda views, a now line and drag to reschedule. Installation: `npx wingo-ui@latest add calendar-view`
- [Calendar](https://wingo-ui.com/components/calendar) (Pro): A React calendar to pick one day, several days or a range, with disabled days, event dots, several months and a quick month / year jump. Installation: `npx wingo-ui@latest add calendar`

## Häufige Fragen

### Welche React Kalender-Komponente mit Terminen ist die beste?

Unter den kostenlosen npm-Paketen stehen react-big-calendar und die Standard-Edition von FullCalendar beide unter MIT und bringen Monats-, Wochen-, Tages- und Agenda- oder Listenansicht mit. Mantine Schedule passt zu Apps, die schon auf Mantine laufen, und die Calendar View von Wingo UI passt, wenn du den Quellcode im eigenen Repo und ein fertiges Smartphone-Layout willst.

### Hat shadcn/ui einen Kalender mit Terminen?

Der Calendar von shadcn ist ein Date Picker auf Basis von React DayPicker, mit Auswahl einzelner Tage und Zeiträume, aber ohne Zeitraster für Woche oder Tag. Für Termine brauchst du einen eigenen Event-Kalender, der die Termine in einem Raster zeigt.

### Was ist eine gute Alternative zu react-big-calendar?

FullCalendar kommt im Funktionsumfang am nächsten heran, und seine Standard-Edition ist unter MIT kostenlos. Wenn du einen Kalender als Quellcode mit Tailwind-Tokens, einer Agenda am Smartphone und Verschieben per Drag willst, ist die Calendar View von Wingo UI eine Pro-Alternative.

### Wie speichere ich das Verschieben per Drag in einem React Kalender?

Aktualisier deine Termine sofort, schick den neuen Start und das neue Ende an den Server und setz das alte Array zurück, wenn die Anfrage scheitert. In der Calendar View bekommt onEventChange den Termin und seine neuen Zeiten, und wenn du false zurückgibst, springt der Termin sofort zurück.

### Wie sollte ein Terminkalender auf dem Smartphone aussehen?

Ein Wochenraster mit sieben Spalten passt nicht auf 390px. Zeig deshalb eine Agenda-Liste nach Tagen, einen einzelnen Tag oder einen kompakten Monat mit Punkten und den Terminen des gewählten Tags darunter. Details zum Termin öffnest du als Bottom Sheet statt als Popover.

---

Quelle: https://wingo-ui.com/de/blog/react-calendar-component-with-events
