Feedback-Formular für eine Website oder App6 Min. Lesezeit
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.
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.
- Der Button ist immer sichtbar, fixiert in der unteren rechten Ecke.
- Ein Klick öffnet das Panel im Top Layer des Browsers und erzeugt einen neuen Versuchsschlüssel.
- Der Nutzer wählt ein Gesicht und schreibt eine Notiz. Der State des Formulars bleibt gemountet, solange das Panel offen oder versteckt ist.
- Beim Absenden ergänzt das Widget Route, App-Version, Konto-ID und Viewport und sendet mit dem SDK.
- Ikiform speichert sie einmal, auch wenn der Button zweimal gedrückt wird, weil beide Posts denselben Idempotency-Key tragen.
- Klickt der Nutzer vorher daneben oder drückt Escape, wird das Panel versteckt, und der Entwurf ist beim nächsten Mal noch da.
Felder für eine App
Vorschau
Führt den Validator von Ikiform in deinem Browser aus. Es wird nichts gesendet.
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.
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 ohnez-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.
onToggleerzeugt bei jedem Öffnen des Panels einen frischen Key.submit()sendet ihn alsIdempotency-Key-Header, sodass ein Doppelklick eine Antwort speichert. Siehe Idempotenz.- 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:
.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:
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.
Prüfen, ob es funktioniert
- 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. - Öffne das Panel, tippe eine Notiz, klick daneben und öffne es erneut. Die Notiz ist noch da.
- Öffne ein Modal in deiner App, dann das Widget. Das Panel erscheint über dem Modal.
- Drück Escape. Das Panel schließt sich, und der Fokus liegt wieder auf dem Feedback-Button.
- Wähle ein Gesicht und doppelklicke auf Senden. Submissions zeigt eine Antwort, mit befüllten
route,app_versionundaccount_id. - 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
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.
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.
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.
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.
Hängst du bei einem Schritt fest oder willst eine Anleitung für deinen Fall? Schreib an hello@ikiform.com.