send.webforms.toRead the docs →

Honeypot field generator

A honeypot field is a form input that people never see and bots fill in, so a submission that fills it is spam. Pick a name and a way to hide it, and get the markup, the CSS and the server-side check for Node, PHP or Python, with the attributes that keep screen reader and keyboard users out of the trap.

Try

How to hide it

Rendered, but positioned out of sight. A bot that only checks whether a field is displayed sees an ordinary one. It relies on aria-hidden and tabindex to stay out of screen readers and the tab order, which is why both are always included.

Inside your form

index.html
<div class="website-field" aria-hidden="true">
  <label for="website-input">Leave this field empty</label>
  <input type="text" id="website-input" name="website" value="" tabindex="-1" autocomplete="off">
</div>

In your stylesheet

styles.css
.website-field {
  position: absolute;
  left: -10000px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

On your server

server.js
import express from "express";

const app = express();
app.use(express.urlencoded({ extended: false }));

app.post("/contact", (req, res) => {
  const trap = req.body?.["website"];

  // A person never sees this field, so anything in it came from a bot.
  // Answer as if it worked: an error tells the bot what caught it.
  if (trap !== undefined && String(trap).trim() !== "") {
    return res.redirect(303, "/thanks");
  }

  // ...validate and handle the real submission here
  res.redirect(303, "/thanks");
});

app.listen(3000);

If your form posts to WebForms

Skip the server code. With the form's Honeypot setting on, which is the default, WebForms checks a field named _gotcha on every submission, so the markup below uses that name in place of yours. A submission that fills it is rejected and never stored, and the sender still gets a normal success response. Keep the stylesheet above.

index.html
<form action="https://send.webforms.to/wft_your_key" method="POST">
  <!-- your real fields -->

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

  <button type="submit">Send</button>
</form>

Generated in your browser. Nothing you type here is sent anywhere.

How a honeypot field works

Most form spam is not aimed at you. A script finds a form, fills every input it can see in the HTML with something plausible and posts the result. A honeypot adds one more input that a visitor never sees. A person leaves it empty because they do not know it is there; a script that fills everything fills it too, and the server discards that submission. It adds no friction and asks nothing of real visitors, which is the main difference when you weigh a honeypot against a captcha. The glossary has the short version: what a honeypot field is.

Choosing a honeypot field name

The name is the part a bot can read, so it should look like a field worth filling. A bot that ignores fields called honeypot, hp or trap needs one line of code to do it, while a field called website or homepage looks like exactly the box a link spammer wants. Any fixed, published name can be skipped by a bot written for it, _gotcha included. Generic bots do not bother, which is why a honeypot still pays for itself, and why it should only ever be the first layer.

Avoid names that browser autofill recognises, such as email, name, phone, address or company. Autofill and password managers can fill a field the visitor never sees, and the honeypot then rejects a real enquiry. autocomplete="off" asks the browser not to, but it is a request rather than a rule, and MDN documents cases where browsers ignore it. The name is the safer guard.

Honeypot field accessibility

A field hidden only from sight is still a field. A screen reader can announce it, a keyboard user can tab into it, and a person who fills it in is treated as a bot. The generated markup closes each route in:

With display: none or the hidden attribute, the field is already out of the accessibility tree, so aria-hidden is redundant. It stays in as a backstop for when a stylesheet fails to load or a display rule elsewhere undoes the hiding. Never use type="hidden" for the trap itself: some bots skip hidden inputs, which defeats the point.

What a honeypot catches and what it misses

It catches the cheap, high-volume end of form spam: scripts that read the HTML and fill every field. It misses anything that behaves like a person. A headless browser driven by a script can work out which fields are visible and fill only those, and someone paid to post spam by hand sees exactly what your visitors see. Neither has any reason to touch the honeypot.

Pair it with a time trap and server-side filtering

A time trap records when the form was rendered and rejects a submission that comes back faster than a person could fill it in, which catches some of the scripts that do render the page. Neither trap reads the content, so the last layer has to: keyword and link checks, sender history, a classifier. All of it has to run on the server, because a bot posting straight to your endpoint never loads your page or its JavaScript.

If your form posts to WebForms, that server side already exists. With the form's Honeypot setting on, which is the default, a submission that fills _gotcha is rejected and never stored, and the sender still gets a normal success response, so the bot learns nothing. The time trap reads a _start_time field, and every other layer is in the spam protection docs. To audit a form you already have, paste it into the form spam checker.