# Schwebendes Feedback-Widget für deine React-App bauen

> Ein schwebender Feedback-Button für eine React-App, der ein kleines Panel öffnet, Ansicht, App-Version und Konto mit jeder Antwort sendet und nie einen Entwurf verliert.

Veröffentlicht am 2026-10-25 · https://ikiform.com/de/guides/feedback-form/react-feedback-widget

Rendere einen fixierten Button, der ein Panel auf Basis der Popover API des Browsers öffnet, damit es ohne z-index-Kampf über jeder Ebene deiner App liegt und sich mit Escape schließt. Ergänze beim Absenden die aktuelle Route, App-Version, Konto-ID und den Viewport in den Formulardaten und sende sie mit dem Ikiform SDK aus dem Browser. Sende die E-Mail des Nutzers nur, wenn er ein Häkchen setzt und um eine Antwort bittet.

## Was an einem Widget in der App anders ist

Ein Formular auf der Seite wartet, bis Leute dorthin scrollen. Ein Widget ist auf jeder Ansicht da, und das ändert, was schiefgehen kann. Es muss über deinen eigenen Modals, Menüs und Toasts liegen. Es darf keine halb geschriebene Nachricht verlieren, wenn jemand daneben klickt. Und weil die Leute angemeldet sind, weißt du schon, wer sie sind und wo sie sind, also sollte das Widget das senden, statt danach zu fragen.

Diagramm: Der Ablauf einer Widget-Sitzung, vom Klick bis zur gespeicherten Antwort

1. Der Button ist immer sichtbar, fixiert in der unteren rechten Ecke.
2. Ein Klick öffnet das Panel im Top Layer des Browsers und erzeugt einen neuen Versuchsschlüssel.
3. Der Nutzer wählt ein Gesicht und schreibt eine Notiz. Der State des Formulars bleibt gemountet, solange das Panel offen oder versteckt ist.
4. Beim Absenden ergänzt das Widget Route, App-Version, Konto-ID und Viewport und sendet mit dem SDK.
5. Ikiform speichert sie einmal, auch wenn der Button zweimal gedrückt wird, weil beide Posts denselben Idempotency-Key tragen.
6. Klickt der Nutzer vorher daneben oder drückt Escape, wird das Panel versteckt, und der Entwurf ist beim nächsten Mal noch da.

Light Dismiss ist das Standardverhalten der Popover API. Weil das Panel nur versteckt wird, behält das Textfeld, was eingetippt wurde.

## Felder für eine App

Formular: In-app feedback

- **Wie läuft es?** (`rating`, rating, Pflichtfeld)
- **Erzähl uns mehr** (`message`, textarea)
- **Das Team darf mir per E-Mail antworten** (`may_reply`, checkbox)
- **E-Mail** (`email`, email)
- **Ansicht** (`route`, text)
- **App-Version** (`app_version`, text)
- **Konto** (`account_id`, text)
- **Viewport** (`viewport`, text)

Nutzer sehen nur die ersten drei Felder. Der Rest wird beim Absenden im Code ergänzt. `may_reply` ist eine Checkbox, gespeichert als `true` oder `false`.

| Feld | Stammt von | Warum |
| --- | --- | --- |
| `rating` | Dem Nutzer, fünf Gesichter | Ein `rating`-Feld mit `max: 5`. Gesichter sind schneller gewählt als Zahlen |
| `message` | Dem Nutzer | Optional. Am nützlichsten bei einem schlechten Gesicht |
| `may_reply`, `email` | Einer Checkbox, dann deiner Session | Die E-Mail wird nur gesendet, wenn um eine Antwort gebeten wird, und Antworten in der Benachrichtigung geht an den Nutzer |
| `route` | `window.location.pathname` | Welche Ansicht. Beim Absenden gelesen, nach jeder clientseitigen Navigation |
| `app_version` | Deinem Build | Zeigt dir, ob sich eine Beschwerde auf Code bezieht, den du schon geändert hast |
| `account_id` | Deiner Session | Lässt dich Tarif und Daten nachschlagen. Kein Identitätsnachweis, siehe unten |
| `viewport` | `window.innerWidth` und `innerHeight` | Layout-Beschwerden hängen oft von der Größe ab |

## Die Komponente

Sie braucht `@ikiform/sdk` und React 19, das `popover`, `popoverTarget` und das `toggle`-Event typisiert. Das `popoverTarget` des Buttons verknüpft ihn mit dem Panel, sodass der Browser Öffnen, Escape und die Rückgabe des Fokus an den Button übernimmt.

`components/feedback-widget.tsx`:

```tsx
import { submit } from "@ikiform/sdk";
import { type FormEvent, type ToggleEvent, useId, useRef, useState } from "react";
import "./feedback-widget.css";

const FORM_ID = "frm_xxxxxxxxxxxxxxxx";
const FACES = ["Awful", "Bad", "Okay", "Good", "Great"];

interface Props {
  accountId: string;
  email: string;
  appVersion: string;
}

export function FeedbackWidget({ accountId, email, appVersion }: Props) {
  const panelId = useId();
  const attempt = useRef("");
  const [status, setStatus] = useState<"idle" | "sending" | "sent">("idle");
  const [error, setError] = useState("");

  function onToggle(event: ToggleEvent<HTMLDivElement>) {
    if (event.newState !== "open") return;
    attempt.current = crypto.randomUUID();
    if (status === "sent") setStatus("idle");
  }

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    if (data.get("may_reply") === "true") data.set("email", email);
    data.set("route", window.location.pathname);
    data.set("app_version", appVersion);
    data.set("account_id", accountId);
    data.set("viewport", `${window.innerWidth}x${window.innerHeight}`);
    setStatus("sending");
    setError("");
    const result = await submit(FORM_ID, data, { idempotencyKey: attempt.current });
    if (result.ok) {
      setStatus("sent");
      return;
    }
    setError(result.error.fields?.rating?.[0] ?? result.error.message);
    setStatus("idle");
  }

  return (
    <>
      <button type="button" className="feedback-trigger" popoverTarget={panelId}>
        Feedback
      </button>
      <div id={panelId} popover="auto" className="feedback-panel" onToggle={onToggle} aria-label="Send feedback">
        {status === "sent" ? (
          <p role="status">Thanks, that helps.</p>
        ) : (
          <form onSubmit={onSubmit}>
            <fieldset>
              <legend>How is it going?</legend>
              {FACES.map((face, index) => (
                <label key={face}>
                  <input type="radio" name="rating" value={index + 1} required /> {face}
                </label>
              ))}
            </fieldset>
            <label htmlFor={`${panelId}-message`}>Tell us more</label>
            <textarea id={`${panelId}-message`} name="message" rows={4} maxLength={4000} />
            <label>
              <input type="checkbox" name="may_reply" value="true" /> The team can reply to me by email
            </label>
            <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
            {error ? <p role="alert">{error}</p> : null}
            <button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Send"}</button>
          </form>
        )}
      </div>
    </>
  );
}
```

- `popover="auto"` legt das Panel beim Öffnen in den Top Layer. Es erscheint ohne `z-index` über deinen Dialogen und Menüs und schließt sich, wenn der Nutzer woanders hinklickt.
- Die Kinder des Panels bleiben gemountet, wenn es versteckt wird, sodass ein Entwurf einen versehentlichen Klick daneben übersteht. Nach erfolgreichem Senden ersetzt die Dankeszeile das Formular, was es leert, und beim nächsten Öffnen erscheint ein leeres Formular.
- `onToggle` erzeugt bei jedem Öffnen des Panels einen frischen Key. `submit()` sendet ihn als `Idempotency-Key`-Header, sodass ein Doppelklick eine Antwort speichert. Siehe [Idempotenz](https://ikiform.com/de/docs/frameworks.md#idempotency).
- Die E-Mail wird nur gesetzt, wenn das Häkchen gesetzt ist. Ohne Häkchen trägt die Antwort die Konto-ID, aber keine Adresse.

Die Standardstyles des Popovers zentrieren es auf dem Bildschirm. Diese Regeln heften es stattdessen über den Button:

`components/feedback-widget.css`:

```text
.feedback-trigger {
  position: fixed;
  right: 16px;
  bottom: 16px;
}

.feedback-panel {
  inset: auto 16px 64px auto;
  margin: 0;
  width: min(360px, calc(100vw - 32px));
  max-height: calc(100dvh - 96px);
  overflow: auto;
}
```

## Einmal mounten

Rendere das Widget im Layout, das die Ansichten für angemeldete Nutzer umschließt, damit es überall erscheint und Navigation übersteht. Im Next.js App Router ist das Layout eine Server-Komponente, die die Session liest und einfache Props an das Client-Widget übergibt:

`app/(app)/layout.tsx`:

```tsx
import type { ReactNode } from "react";
import { FeedbackWidget } from "@/components/feedback-widget";
import { getSession } from "@/lib/session";

export default async function AppLayout({ children }: { children: ReactNode }) {
  const session = await getSession();
  return (
    <>
      {children}
      {session ? (
        <FeedbackWidget
          accountId={session.accountId}
          email={session.email}
          appVersion={process.env.NEXT_PUBLIC_APP_VERSION ?? "dev"}
        />
      ) : null}
    </>
  );
}
```

Markiere die Widget-Datei im App Router mit `"use client"`. In einer Vite- oder React-Router-App renderst du es neben dem Outlet deines Routers. Es funktioniert genauso.

## Was der Browser beweisen kann und was nicht

Dieses Widget sendet mit der öffentlichen ID des Formulars direkt aus dem Browser. Für Feedback ist das der richtige Kompromiss: keine Server-Route zu pflegen, und Ikiforms eigenes `meta.user_agent` ist der echte Browser des Nutzers. Es bedeutet aber auch, dass jeder eine beliebige `account_id` senden kann. Nutze sie zum Nachschlagen, nie, um etwas zu gewähren.

> Muss das Konto vertrauenswürdig sein, etwa um Feedback in einem Admin-Tool anzuzeigen, sende von deinem Server mit dem Secret Key und ergänze das Konto dort aus der Session. Die [Server-Versionen](https://ikiform.com/de/guides/feedback-form.md#form) der Hauptanleitung zeigen das Muster; wähle das Rate-Limit Relaxed oder Custom, denn jeder Post kommt dann von der IP deines Servers.

## Prüfen, ob es funktioniert

1. Füge unter **Allowed domains** des Formulars die Domain deiner Site hinzu, plus `localhost`, während du entwickelst. Lass das Feld leer, um Posts von überall anzunehmen.
2. Öffne das Panel, tippe eine Notiz, klick daneben und öffne es erneut. Die Notiz ist noch da.
3. Öffne ein Modal in deiner App, dann das Widget. Das Panel erscheint über dem Modal.
4. Drück Escape. Das Panel schließt sich, und der Fokus liegt wieder auf dem Feedback-Button.
5. Wähle ein Gesicht und doppelklicke auf Senden. **Submissions** zeigt eine Antwort, mit befüllten `route`, `app_version` und `account_id`.
6. Sende einmal mit gesetztem Antwort-Häkchen. Antworten in der Benachrichtigungs-E-Mail geht an den Nutzer.

## Stolperfallen

- Halte den Button von deinen eigenen schwebenden Elementen fern, etwa einer Chat-Bubble oder einem Cookie-Banner. Verschieb eins davon oder öffne das Widget stattdessen über einen Menüpunkt.
- Zeig den Button nicht auf jeder Ansicht eines Checkouts oder Editors, in dem sich Leute konzentrieren. Blende ihn dort mit einer Prop aus.
- Die Popover API wird in aktuellem Chrome, Edge, Firefox und Safari unterstützt. Musst du ältere Browser unterstützen, prüfe das, bevor du dich darauf verlässt.
- Jedes im Code ergänzte Feld muss in der Definition des Formulars existieren, sonst lehnt Ikiform den Post mit einem Unknown-field-Fehler ab.
- Sende nicht den ganzen App-State. Route, Version, Konto und Größe beantworten die meisten Fragen; alles andere ist eine Datenschutzfrage.

## Fehlerbehebung

| Problem | Was zu tun ist |
| --- | --- |
| `403 origin_not_allowed`, oder jeder Post landet im Spam | Die Domain der Seite steht nicht unter **Allowed domains** des Formulars. Füge sie hinzu, plus `localhost` für lokale Tests. Subdomains einer eingetragenen Domain sind erlaubt. |
| `403 challenge_required` | Die Spamprüfung des Formulars steht auf **Ikiform**, und ein `fetch`-Post hatte kein Token. Hol dir vor jedem Absenden eins mit `challenge(formId)` aus `@ikiform/sdk` oder schalte die Prüfung aus. |
| Das Panel erscheint in der Mitte des Bildschirms | Das Stylesheet wurde nicht geladen, also gelten die Standardstyles des Browsers für Popover. Prüf den CSS-Import oder setze `inset` und `margin` auf dem Panel. |
| `422 validation_failed` | Der Post passt nicht zur Felddefinition des Formulars. `error.fields` nennt jedes Feld und den Grund. Gleiche den `name` des Eingabefelds an die Feld-ID an. |

## Fragen

### Wie füge ich einer React-App einen Feedback-Button hinzu?

Rendere einen fixierten Button, dessen popoverTarget auf ein Panel mit popover=auto zeigt. Der Browser übernimmt Öffnen, Schließen und Fokus. Sende das Formular des Panels mit submit() aus @ikiform/sdk und ergänze beim Absenden Route und App-Version.

### Warum die Popover API statt einer Modal-Bibliothek?

Popover werden im Top Layer gerendert, liegen also ohne z-index-Regeln über deinen eigenen Dialogen, und Light Dismiss und Escape gibt es gratis. Ein Feedback-Panel muss die Seite nicht blockieren, wie es ein Modal tut.

### Ist die Konto-ID im Widget vertrauenswürdig?

Nein. Alles, was aus dem Browser gesendet wird, kann der Nutzer ändern. Nutze sie, um Kontext nachzuschlagen, und sende von deinem Server mit einem Secret Key, wenn der Identität vertraut werden muss.

### Braucht das Widget einen Server?

Nein. Es sendet direkt aus dem Browser an Ikiform, und Allowed domains begrenzt, welche Sites das Formular nutzen können. Ergänze eine Server-Route nur, wenn du eine vertrauenswürdige Identität brauchst.

## Weitere Anleitungen

- [Feedback-Formular für eine Website oder App](https://ikiform.com/de/guides/feedback-form.md): Setze ein Feedback-Formular auf jede Seite oder in deine App: eine Bewertung, ein Kommentar, die Herkunftsseite, Spamfilter und Alerts bei niedrigen Werten.
- [Formular für Bug-Reports](https://ikiform.com/de/guides/feedback-form/bug-report.md): Ein Bug-Report-Formular, das nach Schritten und Schweregrad fragt und URL, Browser, Viewport und letzte Fehler von der Seite selbst ergänzen lässt.
- [NPS-Umfrage](https://ikiform.com/de/guides/feedback-form/nps-survey.md): Führe eine NPS-Umfrage mit einem Wert von 0 bis 10 in der E-Mail durch, mit einer Folgefrage auf deiner eigenen Seite, die sich mit dem Wert ändert, und einem Skript, das den NPS berechnet.
- [Feedback von Beta-Testern](https://ikiform.com/de/guides/feedback-form/beta-tester-feedback.md): Ein Beta-Feedback-Formular mit einem persönlichen Link pro Tester und Build, zwei Bewertungsskalen und einer Wochenübersicht, die zeigt, ob jeder Build besser wurde.
- [Formular für Feature-Wünsche](https://ikiform.com/de/guides/feedback-form/feature-request.md): Ein Formular für Feature-Wünsche, das nach dem Problem statt nach der Lösung fragt, Wünsche nach Thema speichert, Personen statt Zeilen zählt und sie beim Release anschreibt.
- [Kündigungsumfrage](https://ikiform.com/de/guides/feedback-form/cancellation-survey.md): Frag im Kündigungsablauf deines SaaS, warum Kunden kündigen, halte Überspringen einen Klick entfernt, speichere den Grund in Stripe und Ikiform und verzögere die Kündigung nie.
