# Créer un widget de feedback flottant pour votre application React

> Un bouton de feedback flottant pour une application React qui ouvre un petit panneau, envoie l'écran, la version et le compte avec chaque réponse, et ne perd jamais un brouillon.

Publié le 2026-10-25 · https://ikiform.com/fr/guides/feedback-form/react-feedback-widget

Affichez un bouton fixe qui ouvre un panneau construit sur l'API Popover du navigateur : il passe au-dessus de toutes les couches de votre application sans bataille de z-index et se ferme avec Échap. À l'envoi, ajoutez la route courante, la version de l'application, l'id du compte et le viewport aux données du formulaire, puis envoyez-les depuis le navigateur avec le SDK Ikiform. N'envoyez l'e-mail de l'utilisateur que s'il coche une case pour demander une réponse.

## Ce qui change avec un widget dans l'application

Un formulaire de page attend qu'on fasse défiler jusqu'à lui. Un widget est présent sur chaque écran, ce qui change ce qui peut mal tourner. Il doit passer au-dessus de vos propres modales, menus et toasts. Il ne doit pas perdre un message à moitié écrit quand quelqu'un clique à côté. Et comme les gens sont connectés, vous savez déjà qui ils sont et où ils se trouvent : le widget doit donc l'envoyer au lieu de le demander.

Schéma : La vie d'une session du widget de feedback, du clic à la réponse stockée

1. Le bouton est toujours à l'écran, fixé dans le coin inférieur droit.
2. Un clic ouvre le panneau dans la top layer du navigateur et crée une nouvelle clé de tentative.
3. L'utilisateur choisit un visage et écrit une note. L'état du formulaire reste monté, que le panneau soit ouvert ou masqué.
4. À l'envoi, le widget ajoute la route, la version de l'application, l'id du compte et le viewport, et envoie avec le SDK.
5. Ikiform le stocke une seule fois, même si le bouton est pressé deux fois, car les deux envois portent la même clé d'idempotence.
6. Si l'utilisateur clique à côté ou appuie d'abord sur Échap, le panneau se masque et le brouillon est toujours là la fois suivante.

La fermeture au clic extérieur est le comportement par défaut de l'API Popover. Comme le panneau est seulement masqué, le champ de texte garde ce qui a été tapé.

## Des champs pour une application

Formulaire : In-app feedback

- **Comment ça se passe ?** (`rating`, rating, obligatoire)
- **Dites-nous en plus** (`message`, textarea)
- **L'équipe peut me répondre par e-mail** (`may_reply`, checkbox)
- **E-mail** (`email`, email)
- **Écran** (`route`, text)
- **Version de l'application** (`app_version`, text)
- **Compte** (`account_id`, text)
- **Viewport** (`viewport`, text)

Les utilisateurs ne voient que les trois premiers champs. Les autres sont ajoutés dans le code à l'envoi. `may_reply` est une case à cocher, stockée comme `true` ou `false`.

| Champ | Provient de | Pourquoi |
| --- | --- | --- |
| `rating` | L'utilisateur, cinq visages | Un champ `rating` avec `max: 5`. Un visage se choisit plus vite qu'un chiffre |
| `message` | La personne elle-même | Facultatif. Surtout utile quand le visage est mécontent |
| `may_reply`, `email` | Une case à cocher, puis votre session | L'e-mail n'est envoyé que s'ils demandent une réponse, et Répondre dans la notification leur écrit |
| `route` | `window.location.pathname` | Quel écran. Lu à l'envoi, après toute navigation côté client |
| `app_version` | Votre build | Vous dit si une plainte concerne du code que vous avez déjà modifié |
| `account_id` | Votre session | Permet de retrouver son offre et ses données. Ce n'est pas une preuve d'identité, voir plus bas |
| `viewport` | `window.innerWidth` et `innerHeight` | Les plaintes de mise en page dépendent souvent de la taille |

## Le composant

Il a besoin de `@ikiform/sdk` et de React 19, qui type `popover`, `popoverTarget` et l'événement `toggle`. Le `popoverTarget` du bouton le relie au panneau, donc le navigateur gère l'ouverture, Échap et le retour du focus sur le bouton.

`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"` place le panneau dans la top layer quand il est ouvert. Il s'affiche au-dessus de vos dialogues et menus sans `z-index`, et se ferme quand l'utilisateur clique ailleurs.
- Les enfants du panneau restent montés quand il est masqué : un brouillon survit donc à un clic extérieur accidentel. Après un envoi réussi, la ligne de remerciement remplace le formulaire, ce qui le vide, et l'ouverture suivante affiche un formulaire vierge.
- `onToggle` crée une nouvelle clé à chaque ouverture du panneau. `submit()` l'envoie dans l'en-tête `Idempotency-Key`, donc un double clic ne stocke qu'une réponse. Voir [idempotence](https://ikiform.com/fr/docs/frameworks.md#idempotency).
- L'e-mail n'est renseigné que si la case est cochée. Décochée, la réponse porte l'id du compte mais aucune adresse.

Les styles par défaut du popover le centrent à l'écran. Ces règles l'ancrent plutôt au-dessus du bouton :

`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;
}
```

## Le monter une seule fois

Affichez le widget dans le layout qui enveloppe les écrans connectés, pour qu'il apparaisse partout et survive à la navigation. Dans le Next.js App Router, le layout est un server component qui lit la session et passe des props simples au widget client :

`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}
    </>
  );
}
```

Marquez le fichier du widget avec `"use client"` dans l'App Router. Dans une application Vite ou React Router, affichez-le à côté de l'outlet de votre routeur. Il fonctionne de la même façon.

## Ce que le navigateur peut prouver ou non

Ce widget envoie directement depuis le navigateur avec l'id public du formulaire. C'est le bon compromis pour du feedback : aucune route serveur à maintenir, et le `meta.user_agent` d'Ikiform est le vrai navigateur de l'utilisateur. Mais cela veut aussi dire que n'importe qui peut envoyer n'importe quel `account_id`. Servez-vous-en pour retrouver des informations, jamais pour accorder quoi que ce soit.

> Si le compte doit être fiable, par exemple pour afficher le feedback dans un outil d'administration, envoyez depuis votre serveur avec la clé secrète et ajoutez-y le compte depuis la session. Les [versions serveur](https://ikiform.com/fr/guides/feedback-form.md#form) du guide principal montrent comment faire ; choisissez la limite de débit Relaxed ou Custom, car chaque envoi vient alors de l'IP de votre serveur.

## Vérifiez que tout fonctionne

1. Dans **Allowed domains** du formulaire, ajoutez le domaine de votre site, plus `localhost` pendant le développement. Laissez vide pour accepter les envois de partout.
2. Ouvrez le panneau, tapez une note, cliquez à côté, puis rouvrez-le. La note est toujours là.
3. Ouvrez une modale dans votre application, puis le widget. Le panneau s'affiche au-dessus de la modale.
4. Appuyez sur Échap. Le panneau se ferme et le focus revient sur le bouton Feedback.
5. Choisissez un visage et double-cliquez sur Envoyer. **Submissions** affiche une seule réponse, avec `route`, `app_version` et `account_id` remplis.
6. Envoyez une fois avec la case de réponse cochée. Le bouton Répondre de l'e-mail de notification écrit à l'utilisateur.

## Pièges

- Éloignez le bouton de vos propres éléments flottants, comme une bulle de chat ou une bannière de cookies. Déplacez l'un d'eux, ou ouvrez plutôt le widget depuis un élément de menu.
- N'affichez pas le bouton sur chaque écran d'un checkout ou d'un éditeur où les gens sont concentrés. Masquez-le là avec une prop.
- L'API Popover est prise en charge par les versions actuelles de Chrome, Edge, Firefox et Safari. Si vous devez prendre en charge des navigateurs plus anciens, vérifiez avant de vous y fier.
- Chaque champ ajouté dans le code doit exister dans la définition du formulaire, sinon Ikiform refuse l'envoi avec une erreur Unknown field.
- N'envoyez pas tout l'état de l'application. Route, version, compte et taille répondent à la plupart des questions ; le reste pose une question de confidentialité.

## Dépannage

| Problème | Que faire |
| --- | --- |
| `403 origin_not_allowed`, ou tous les envois finissent en spam | Le domaine de la page n'est pas dans les **Allowed domains** du formulaire. Ajoutez-le, plus `localhost` pour les tests en local. Les sous-domaines d'un domaine listé sont autorisés. |
| `403 challenge_required` | Le spam check du formulaire est réglé sur **Ikiform** et un envoi `fetch` n'avait pas de jeton. Obtenez-en un avec `challenge(formId)` de `@ikiform/sdk` avant chaque envoi, ou désactivez la vérification. |
| Le panneau apparaît au milieu de l'écran | La feuille de style ne s'est pas chargée, donc ce sont les styles par défaut du popover qui s'appliquent. Vérifiez l'import CSS, ou définissez `inset` et `margin` sur le panneau. |
| `422 validation_failed` | L'envoi ne correspond pas à la définition des champs du formulaire. `error.fields` nomme chaque champ et la raison. Faites correspondre le `name` de l'input à l'id du champ. |

## Questions

### Comment ajouter un bouton de feedback à une application React ?

Affichez un bouton fixe dont le popoverTarget pointe vers un panneau doté de popover=auto. Le navigateur gère l'ouverture, la fermeture et le focus. Envoyez le formulaire du panneau avec submit() de @ikiform/sdk et ajoutez la route et la version de l'application à l'envoi.

### Pourquoi utiliser l'API Popover plutôt qu'une bibliothèque de modales ?

Les popovers s'affichent dans la top layer : ils passent au-dessus de vos propres dialogues sans règles de z-index, et la fermeture au clic extérieur et Échap sont fournies d'office. Un panneau de feedback n'a pas besoin de bloquer la page comme le fait une modale.

### L'id de compte du widget est-il fiable ?

Non. Tout ce qui est envoyé depuis le navigateur peut être modifié par l'utilisateur. Servez-vous-en pour retrouver du contexte, et envoyez depuis votre serveur avec une clé secrète quand l'identité doit être fiable.

### Le widget a-t-il besoin d'un serveur ?

Non. Il envoie directement à Ikiform depuis le navigateur, et Allowed domains limite les sites qui peuvent utiliser le formulaire. N'ajoutez une route serveur que si vous avez besoin d'une identité fiable.

## Autres guides

- [Formulaire de feedback pour un site ou une application](https://ikiform.com/fr/guides/feedback-form.md): Placez un formulaire de feedback sur n'importe quelle page ou dans votre application : une note, un commentaire, la page d'origine, le filtrage du spam et des alertes quand une note est basse.
- [Formulaire de signalement de bug](https://ikiform.com/fr/guides/feedback-form/bug-report.md): Un formulaire de signalement de bug qui demande les étapes et la gravité, et laisse la page ajouter elle-même l'URL, le navigateur, le viewport et les erreurs récentes.
- [Enquête NPS](https://ikiform.com/fr/guides/feedback-form/nps-survey.md): Menez une enquête NPS avec un score de 0 à 10 dans l'e-mail, une question de suivi sur votre propre page qui change selon le score, et un script qui calcule le NPS.
- [Feedback des bêta-testeurs](https://ikiform.com/fr/guides/feedback-form/beta-tester-feedback.md): Un formulaire de feedback bêta avec un lien personnel par testeur et par build, deux échelles de notation et un résumé hebdomadaire qui montre si chaque build s'est amélioré.
- [Formulaire de demande de fonctionnalité](https://ikiform.com/fr/guides/feedback-form/feature-request.md): Un formulaire de demande de fonctionnalité qui demande le problème plutôt que la solution, range les demandes par thème, compte les personnes et non les lignes, et les prévient quand c'est livré.
- [Enquête de résiliation](https://ikiform.com/fr/guides/feedback-form/cancellation-survey.md): Demandez pourquoi vos clients résilient dans le parcours d'annulation de votre SaaS, gardez Passer à un clic, enregistrez la raison dans Stripe et Ikiform, sans jamais retarder la résiliation.
