Formulaire de contact avec fetch : confirmations et gestion des erreurs

Créez un formulaire de contact HTML avec fetch, des confirmations claires et des erreurs utiles. Copiez un exemple complet qui conserve le message du visiteur si l’envoi échoue.

Par l’équipe mailcontact

Un formulaire de contact doit répondre clairement à une question : le message est-il arrivé ? Utilisez fetch pour envoyer sans quitter la page, afficher un état de chargement et confirmer seulement après acceptation par le point de réception. En cas d’échec, gardez le texte et indiquez la marche à suivre.

Voici un exemple complet HTML et JavaScript pour le point de réception mailcontact. Il ne nécessite aucun framework frontend. Il inclut des libellés visibles, la validation du navigateur, un champ piège, une protection contre les doubles clics et un message de statut sur place. Remplacez l’URL d’exemple par celle de votre projet avant de tester.

Pourquoi un fetch résolu ne suffit pas

Une requête peut atteindre le serveur tout en voyant son envoi refusé. Par exemple, un email invalide renvoie HTTP 400 et un quota mensuel épuisé renvoie HTTP 429. fetch ne rejette pas sa promesse simplement parce que la réponse a un statut d’erreur HTTP. Vérifiez response.ok et le corps de réponse attendu avant de réinitialiser le formulaire. Ce comportement est décrit dans Guide Fetch de MDN.

mailcontact renvoie {"ok":true} pour les requêtes acceptées. Un champ piège rempli reçoit volontairement la même réponse tout en étant ignoré : vérifiez aussi les erreurs de remplissage automatique. Les visiteurs normaux doivent laisser ce champ vide. Le référence du point de réception décrit le format de réponse et les limites de validation.

Un formulaire de contact complet avec messages de succès et d’erreur

Pour une nouvelle intégration, privilégiez le script MailContact hébergé ou SDK personnalisé. Il injecte le contrôle caché, effectue la vérification automatique et conserve les brouillons en cas d’erreur. Les intégrations Free avec script doivent afficher un lien visible « Traité par mailcontact.app ». L’exemple fetch brut ci-dessous illustre les réponses HTTP ; il inclut le contrôle requis, mais les messages suspects sans prise en charge de la vérification sont placés dans les spams pour examen.

contact.html
<form id="contact-form" action="https://mailcontact.app/api/forms/your-form-id" method="POST">
  <fieldset>
    <legend>Contact us</legend>

    <label for="contact-name">Name (optional)</label>
    <input id="contact-name" name="name" autocomplete="name" maxlength="200" />

    <label for="contact-email">Your email</label>
    <input id="contact-email" type="email" name="email"
      autocomplete="email" maxlength="320" required />

    <label for="contact-message">Message</label>
    <textarea id="contact-message" name="message" maxlength="5000" required></textarea>

    <div class="contact-meta" aria-hidden="true">
      <label for="contact-website">Website</label>
      <input id="contact-website" name="company_website" type="text"
        tabindex="-1" autocomplete="off" />
    </div>

    <button type="submit">Send message</button>
  </fieldset>
  <p id="contact-status" role="status" aria-live="polite" aria-atomic="true"></p>
</form>

<style>
  .contact-meta {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
  }
</style>

<!-- Keep the script after the form so its elements exist. -->
<script>
  const form = document.querySelector("#contact-form");
  const fields = form.querySelector("fieldset");
  const button = form.querySelector('button[type="submit"]');
  const status = document.querySelector("#contact-status");
  let sending = false;

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    if (sending || !form.reportValidity()) return;

    // Collect values before disabling the fieldset.
    const body = new FormData(form);
    if (!String(body.get("message") || "").trim()) {
      status.textContent = "Please write a message before sending.";
      form.elements.namedItem("message").focus();
      return;
    }

    sending = true;
    fields.disabled = true;
    form.setAttribute("aria-busy", "true");
    button.textContent = "Sending…";
    status.textContent = "Sending your message…";
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 15000);

    try {
      const response = await fetch(form.action, {
        method: "POST",
        body,
        signal: controller.signal,
      });
      const data = await response.json().catch(() => null);

      if (!response.ok || data?.ok !== true) {
        let message = "Your message could not be sent. Your text is still here.";
        if (response.status === 400) {
          message = "Check your email and message length, then try again.";
        } else if (response.status === 429) {
          message = data?.code === "MONTHLY_QUOTA_EXCEEDED"
            ? "This form has reached its monthly limit. Please use another contact method on this site."
            : "Too many requests. Please wait before trying again.";
        } else if (response.status === 404) {
          message = "This contact form is unavailable. Please use another contact method on this site.";
        }
        status.textContent = message;
        return;
      }

      form.reset();
      status.textContent = "Thanks! Your message has been received.";
    } catch {
      status.textContent = "We could not confirm delivery. Your text is still here. Check your connection before trying again.";
    } finally {
      clearTimeout(timeout);
      sending = false;
      fields.disabled = false;
      form.removeAttribute("aria-busy");
      button.textContent = "Send message";
    }
  });
</script>

Conservez vos styles habituels autour de ce HTML. La petite règle CSS ne concerne que le champ piège, sans imposer de mise en page. Sans JavaScript, le formulaire envoie toujours nativement, mais le point de réception actuel affiche du JSON au lieu de rediriger vers une page de remerciement.

Que se passe-t-il pendant l’envoi ?

Le gestionnaire collecte les champs avant de désactiver le fieldset. Les champs désactivés sont exclus de FormData : inverser l’ordre perdrait les données. Cela empêche aussi de nouvelles modifications pendant la requête, qui seraient effacées par la réinitialisation après succès. Le composant séparé sending la protection empêche les envois simultanés.

La zone de statut est présente au chargement et son texte évolue avec l’état. Elle fournit un retour sans boîte d’alerte ni simple changement de couleur. Le finally le bloc rétablit le formulaire après un succès, une erreur serveur ou réseau : le bouton ne reste pas bloqué.

Conservez le message si la livraison est incertaine

Le délai de 15 secondes évite une attente indéfinie. Il ne peut pas annuler un message déjà accepté. La connexion peut aussi échouer après l’enregistrement mais avant la confirmation au navigateur. L’erreur réseau indique donc que la livraison n’a pas pu être confirmée, sans prétendre qu’aucun message n’a été envoyé.

L’exemple ne retente pas automatiquement les requêtes incertaines : un deuxième essai peut créer un doublon. Le brouillon reste pour laisser le choix au visiteur. Pour garantir la déduplication entre tentatives, il faut un mécanisme explicite d’idempotence côté réception ; désactiver le bouton ne suffit pas.

Distinguez les limites temporaires et les quotas mensuels

Une réponse 429 peut signaler une limitation temporaire du débit ou un quota mensuel épuisé. Consultez le JSON code: MONTHLY_QUOTA_EXCEEDED identifie ce dernier cas. Demander de réessayer quelques secondes plus tard est inutile si le formulaire a atteint son quota mensuel.

Pour les limites de débit temporaires, le point de réception fournit un Retry-After en-tête. Il permet d’ajouter un compte à rebours si votre interface en a besoin. Pour un quota épuisé, proposez un vrai moyen de contact alternatif sur votre site. Le formulaire et l’adresse de réception d’un projet Pro partagent le quota du compte : cette adresse n’est donc pas une solution de repli. Consultez votre utilisation et la limites des offres avant un lancement important.

Vérifiez le formulaire avant de publier

  • Envoi réussi : la confirmation apparaît, le formulaire se vide et le message est dans la boîte du projet.
  • Données invalides : un email invalide ou un message vide produit un retour utile en conservant les autres champs.
  • Navigateur hors ligne : le brouillon reste, la livraison est signalée comme incertaine et le bouton redevient utilisable.
  • Clics répétés : une seule requête est en cours pendant l’état d’envoi.
  • Livraison des notifications : une destination Slack ou Discord configurée reçoit une alerte après l’acceptation d’un message.

N’affichez pas un succès simplement parce qu’une animation se termine ou qu’un bouton est cliqué. Basez-le sur la réponse du point de réception, permettez de réessayer et vérifiez le parcours jusqu’à votre boîte. Pour la réception, suivez le Guide des notifications Slack ou le Guide Discord.

Connectez votre formulaire à une boîte de projet.

Gardez le design de votre formulaire et laissez mailcontact recevoir les messages. Créez un compte pour accéder au lancement.

Créer mon formulaire

Aucune carte bancaire requise. Free comprend un projet et 50 messages de formulaire par mois.