# Comment ajouter un champ honeypot que les bots remplissent et pas les humains

> Ajoutez à n'importe quel formulaire un honeypot qui attrape les bots sans gêner lecteurs d'écran, clavier ou remplissage automatique, avec la vérification serveur pour chaque stack.

Publié le 2026-10-25 · https://ikiform.com/fr/guides/spam-protection/honeypot

Ajoutez un input texte supplémentaire que vous masquez hors écran, retirez de l'ordre de tabulation et englobez dans `aria-hidden`, avec un nom qu'aucun navigateur ne remplit automatiquement. Les personnes ne le remplissent jamais ; les bots qui remplissent chaque input, si. Côté serveur, traitez tout envoi où il a une valeur comme du spam et répondez exactement comme pour un vrai message.

## Ce qui distingue un honeypot

Un honeypot doit être deux choses à la fois. Pour un bot qui lit le HTML, il doit ressembler à un champ normal qui mérite d'être rempli. Pour chaque personne, il ne doit pas exister, y compris pour celles qui n'utilisent pas le formulaire comme vous : quelqu'un avec un lecteur d'écran, quelqu'un qui navigue au clavier, quelqu'un dont le gestionnaire de mots de passe remplit les formulaires à sa place. La plupart des bugs de honeypot viennent de l'oubli d'une de ces personnes, et le symptôme est un vrai message discrètement classé en spam.

Schéma : Comment une personne et un bot rencontrent le même champ honeypot

1. Une personne remplit le formulaire avec ce qu'elle utilise : un écran, un clavier ou un lecteur d'écran.
2. Le piège est hors écran, hors de l'ordre de tabulation et masqué aux technologies d'assistance : elle ne l'atteint jamais.
3. Son nom est de ceux que les navigateurs et gestionnaires de mots de passe ne remplissent pas automatiquement : il reste donc vide.
4. L'envoi arrive avec un piège vide et est reçu.
5. Un bot scripté analyse le HTML et trouve chaque input, piège compris.
6. Il les remplit tous et envoie aussitôt.
7. Le piège a une valeur : l'envoi est donc du spam. Le bot reçoit quand même une réponse de succès normale.

Chaque garde-fou de la ligne du haut ferme une voie par laquelle une vraie personne pourrait atteindre le champ. Retirez-en un et la ligne du haut peut aussi finir en spam.

## Le balisage

Placez le piège dans le formulaire, à côté des vrais champs, sous forme d'input texte simple. Un input `type="hidden"` ne fonctionne pas : les bots savent que les personnes ne peuvent pas les modifier, et ne les touchent pas.

`contact.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" required></textarea>

  <div class="hp" aria-hidden="true">
    <label for="hp-field">Leave this empty</label>
    <input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
  </div>

  <button>Send</button>
</form>
```

`styles`:

```html
<style>
.hp {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
</style>
```

- `aria-hidden="true"` sur le conteneur retire le label et l'input de l'arbre d'accessibilité : les lecteurs d'écran ne les annoncent donc pas.
- `tabindex="-1"` empêche la touche Tab de s'arrêter sur un input que personne ne voit.
- Le label dit **Leave this empty** (laissez vide). Si un outil expose malgré tout le champ, la personne qui le lit sait quoi faire.
- N'ajoutez pas `required`. Un champ obligatoire que personne ne voit bloque tout envoi honnête.

## Comment le masquer

Les bots ont appris à ignorer les champs manifestement masqués. Les scrapers simples remplissent encore tout, mais certains vérifient l'attribut `hidden` ou un `display: none` inline avant de taper. Plus le masquage est éloigné de l'input lui-même, plus les bots s'y laissent prendre.

| Technique | Masqué aux personnes | Bots encore attrapés |
| --- | --- | --- |
| Attribut `hidden` ou `display: none` inline | Oui | Seulement les plus simples. C'est la première chose qu'un bot plus malin vérifie |
| Classe hors écran dans votre feuille de style | Oui | La plupart. Le bot doit charger et appliquer votre CSS pour le savoir |
| `opacity: 0` ou taille nulle | En grande partie. Certaines loupes et contours de focus le révèlent encore | La plupart, mais plus risqué pour les personnes |
| Ajouté par JavaScript après le chargement | Oui | Les bots qui exécutent des scripts. Les scrapers HTML simples ne le voient jamais : il les rate donc |

Utilisez la classe hors écran de votre feuille de style. Elle attrape le plus de bots et c'est la plus sûre pour les personnes. Si un honeypot cesse de fonctionner, ajoutez-en un second avec un autre nom plutôt qu'une astuce de masquage plus subtile.

## Choisir un nom que le remplissage automatique ignore

Les navigateurs et les gestionnaires de mots de passe remplissent les champs en devinant d'après leur `name`, leur `id`, leur label et leur valeur `autocomplete`. `autocomplete="off"` n'est qu'une indication, et certains navigateurs l'ignorent pour les champs qui ressemblent à des adresses ou des coordonnées. C'est donc le nom qui fait le vrai travail.

| À éviter | Pourquoi | À utiliser à la place |
| --- | --- | --- |
| `email2`, `phone`, `name`, `address` | Le remplissage automatique les reconnaît et les remplit pour de vraies personnes | `_gotcha`, `company_website` |
| `honeypot`, `trap`, `bot` | Certains bots ignorent les champs aux noms évidents | Un nom banal et plausible |
| `url` ou `website` seuls | Certains outils de remplissage automatique remplissent un champ site web à partir d'un profil enregistré | Un nom composé comme `company_website` |

## Le vérifier sur le serveur

Un honeypot ne fonctionne que si le serveur le lit. Une vérification côté navigateur ne sert à rien, puisqu'un bot n'exécute jamais votre script. Quand le champ a une valeur, répondez exactement comme pour un vrai message, et ne stockez ni ne transmettez l'envoi. Une réponse différente indique au bot quel champ l'a trahi. Les exemples ci-dessous appellent votre propre `saveMessage` ; remplacez-le par ce qui stocke ou envoie le message par e-mail.

**HTML**

Un formulaire HTML simple n'a pas de serveur à lui. Faites-le pointer vers Ikiform et nommez le piège `_gotcha`, ou renommez-le pour chaque formulaire sous **Honeypot field**. Ikiform fait la vérification serveur, et le piège n'est jamais stocké comme réponse.

`contact.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" required></textarea>

  <div class="hp" aria-hidden="true">
    <label for="hp-field">Leave this empty</label>
    <input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
  </div>

  <button>Send</button>
</form>
```

**Next.js**

Dans une server action, redirigez un envoi piégé vers la même page de remerciement qu'un vrai, et sautez l'enregistrement.

`app/contact/actions.ts`:

```ts
"use server";

import { redirect } from "next/navigation";

export async function sendMessage(formData: FormData) {
  if (String(formData.get("company_website") ?? "").trim() !== "") {
    console.info("honeypot", { at: new Date().toISOString() });
    redirect("/contact/thanks");
  }
  await saveMessage({
    email: String(formData.get("email") ?? ""),
    message: String(formData.get("message") ?? ""),
  });
  redirect("/contact/thanks");
}
```

**Astro**

Dans une Astro Action, retirez le piège de l'input et renvoyez le résultat normal quand il a une valeur.

`src/actions/index.ts`:

```ts
import { defineAction } from "astro:actions";
import { z } from "astro/zod";

export const server = {
  sendMessage: defineAction({
    accept: "form",
    input: z.object({
      email: z.email(),
      message: z.string().min(1),
      company_website: z.string().nullish(),
    }),
    handler: async ({ company_website, ...fields }) => {
      if (company_website?.trim()) return { sent: true };
      await saveMessage(fields);
      return { sent: true };
    },
  }),
};
```

**TanStack Start**

Lisez le piège dans `.validator()` et sautez l'enregistrement dans le handler. Les deux chemins renvoient la même réponse.

`src/server/send-message.ts`:

```ts
import { createServerFn } from "@tanstack/react-start";

export const sendMessage = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    return {
      email: String(data.get("email") ?? ""),
      message: String(data.get("message") ?? ""),
      trapped: String(data.get("company_website") ?? "").trim() !== "",
    };
  })
  .handler(async ({ data: { trapped, ...fields } }) => {
    if (!trapped) await saveMessage(fields);
    return { ok: true };
  });
```

**React Router**

L'action de la route redirige vers la page de remerciement dans les deux cas et n'enregistre que les envois non piégés.

`app/routes/contact.tsx`:

```ts
import { redirect } from "react-router";
import type { Route } from "./+types/contact";

export async function action({ request }: Route.ActionArgs) {
  const form = await request.formData();
  if (String(form.get("company_website") ?? "").trim()) return redirect("/contact/thanks");
  await saveMessage({ email: String(form.get("email")), message: String(form.get("message")) });
  return redirect("/contact/thanks");
}
```

**SvelteKit**

La form action n'enregistre que si le piège est vide, puis redirige avec `303` dans les deux cas.

`src/routes/contact/+page.server.ts`:

```ts
import { redirect } from "@sveltejs/kit";
import type { Actions } from "./$types";

export const actions = {
  default: async ({ request }) => {
    const form = await request.formData();
    if (!String(form.get("company_website") ?? "").trim()) {
      await saveMessage({ email: String(form.get("email")), message: String(form.get("message")) });
    }
    redirect(303, "/contact/thanks");
  },
} satisfies Actions;
```

**Nuxt**

La server route répond `201` avec le même corps pour un envoi piégé et pour un vrai.

`server/api/contact.post.ts`:

```ts
export default defineEventHandler(async (event) => {
  const body = await readBody<{ email?: string; message?: string; company_website?: string }>(event);
  if (body.company_website?.trim()) {
    setResponseStatus(event, 201);
    return { sent: true };
  }
  await saveMessage({ email: body.email ?? "", message: body.message ?? "" });
  setResponseStatus(event, 201);
  return { sent: true };
});
```

**React**

Une application monopage ne peut pas vérifier le piège elle-même. Envoyez-le avec les autres champs, et laissez décider le serveur qui reçoit l'envoi. `FormData` l'inclut sans code supplémentaire.

`submit.ts`:

```ts
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
  method: "POST",
  headers: { Accept: "application/json" },
  body: new FormData(form),
});
```

**Vue**

Même chose en Vue : gardez le piège dans le `<form>` pour que `new FormData(form)` l'inclue, et laissez le serveur le vérifier.

`submit.ts`:

```ts
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
  method: "POST",
  headers: { Accept: "application/json" },
  body: new FormData(form),
});
```

> Journalisez les envois piégés, même si vous ne les stockez pas. Un décompte par jour vous dit si le honeypot mérite encore sa place, et une chute soudaine à zéro signifie que les bots ont appris à l'ignorer.

## Comment Ikiform gère le honeypot

Chaque formulaire Ikiform a par défaut un honeypot nommé `_gotcha`. Changez-le pour chaque formulaire sous **Honeypot field** dans les réglages du formulaire ; le nom choisi est réservé et n'est jamais stocké comme réponse. Un honeypot rempli ajoute 5 au score de spam, ce qui atteint à lui seul le seuil de 5.

Ikiform ne jette pas l'envoi. Il le garde comme spam : si une vraie personne déclenche un jour le piège, vous pouvez retrouver le message et le restaurer. Un envoi jugé comme spam reçoit la même réponse de succès qu'un vrai : un bot n'apprend donc rien. Il est stocké avec le statut `spam`, n'envoie ni e-mail de notification ni webhook, ne compte pas dans votre consommation, et est supprimé au bout de 30 jours. Chaque lundi, un espace de travail qui a bloqué du spam reçoit un e-mail récapitulatif avec le total, les formulaires les plus sollicités et les principales raisons. La [documentation sur le spam](https://ikiform.com/fr/docs/spam.md#honeypot) et celle des [formulaires HTML](https://ikiform.com/fr/docs/html.md#honeypot) servent de référence.

## Vérifiez que tout fonctionne

1. Chargez le formulaire avec un lecteur d'écran activé et parcourez-le. Le piège n'est jamais annoncé.
2. Parcourez le formulaire avec Tab. Le focus passe du dernier vrai champ directement au bouton.
3. Laissez votre navigateur et votre gestionnaire de mots de passe remplir automatiquement le formulaire. Le piège reste vide.
4. Envoyez avec le honeypot rempli, `_gotcha=bot`. L'envoi reçoit quand même une réponse de succès, mais la soumission est stockée comme spam et n'envoie ni e-mail ni webhook.
5. Envoyez un message normal. Il arrive comme reçu, sans trace du champ piège.

## Pièges

- **Envois JSON.** Si vous construisez le corps à la main au lieu d'utiliser `FormData`, incluez le piège, sinon votre serveur le lira comme vide pour les bots aussi.
- **Créateurs de formulaires qui renomment les champs.** Certains outils préfixent ou renomment les inputs. Vérifiez le vrai nom du piège dans le corps envoyé, pas dans l'éditeur.
- **Cache.** Si une page en cache garde l'ancien nom du piège après que vous l'avez renommé, les envois arrivent avec l'ancien nom et passent. Renommez des deux côtés en même temps.
- **Honeypots en case à cocher.** Une case à cocher masquée fonctionne aussi, mais les lecteurs d'écran gèrent moins bien les cases isolées que les inputs texte. Préférez un input texte.
- **Une seule couche.** Les bots qui pilotent un vrai navigateur et ignorent les champs invisibles passent outre tout honeypot. Associez-le à une [limite de débit](https://ikiform.com/fr/guides/spam-protection/rate-limiting.md) et, si besoin, à une [vérification humaine](https://ikiform.com/fr/guides/spam-protection/cloudflare-turnstile.md).

## Questions

### Les champs honeypot fonctionnent-ils encore contre les bots de spam ?

Oui, contre les plus courants : les scripts qui remplissent chaque input trouvé. Les bots qui pilotent un navigateur complet et ignorent les champs invisibles passent : utilisez donc un honeypot comme première couche, pas comme la seule.

### Un champ honeypot nuit-il à l'accessibilité ?

Seulement s'il est masqué à la légère. Englobez-le dans un élément avec aria-hidden, donnez à l'input tabindex -1, mettez le label Leave this empty, et ne le rendez jamais obligatoire. Les lecteurs d'écran et le clavier l'ignorent alors.

### Pourquoi un vrai message a-t-il été attrapé par mon honeypot ?

En général, à cause du remplissage automatique. Un navigateur ou un gestionnaire de mots de passe a rempli le piège parce que son nom ressemblait à un vrai champ, comme email2 ou phone. Renommez-le en quelque chose qu'aucun remplissage automatique ne reconnaît.

### Dois-je dire à l'expéditeur que son message a été rejeté ?

Non. Répondez à un envoi piégé exactement comme à un vrai. Une erreur indique au bot quel champ l'a attrapé, et la version suivante du bot ignore ce champ.

## Autres guides

- [Arrêter le spam des formulaires de contact](https://ikiform.com/fr/guides/spam-protection.md): Arrêtez le spam des formulaires de contact avec des couches invisibles pour les vrais visiteurs : honeypot, délai, score du contenu, limites de débit, vérification humaine si besoin.
- [Sans captcha](https://ikiform.com/fr/guides/spam-protection/without-captcha.md): Arrêtez l'essentiel du spam sans captcha : un honeypot, une heure de rendu signée, un score du contenu et une limite de débit, cumulés plutôt qu'utilisés seuls.
- [Limitation de débit](https://ikiform.com/fr/guides/spam-protection/rate-limiting.md): Limitez les envois de formulaire par visiteur avec un stockage partagé, répondez 429 avec Retry-After, et évitez de bloquer les bureaux qui partagent une IP.
- [Cloudflare Turnstile](https://ikiform.com/fr/guides/spam-protection/cloudflare-turnstile.md): Ajoutez Cloudflare Turnstile à un formulaire HTML simple : le widget, siteverify côté serveur, les clés de test, et une façon de l'utiliser sans clés ni serveur à vous.
- [Alternatives à reCAPTCHA](https://ikiform.com/fr/guides/spam-protection/recaptcha-alternatives.md): Comparez les alternatives à reCAPTCHA pour formulaires, de Turnstile et hCaptcha à la preuve de travail ou à l'absence de captcha, puis migrez avec un petit changement sûr.
- [Sites statiques](https://ikiform.com/fr/guides/spam-protection/static-site.md): Arrêtez le spam d'un formulaire de site statique, même sans secret ni compteur, avec un partial honeypot, un script sûr et des vérifications côté serveur.
