Stop contact form spam

Stop contact form spam6 min read

How to add a honeypot field that bots fill and people don't

Add a honeypot to any form that catches bots without tripping screen readers, keyboards or autofill, with the server check for every stack.

Preet SutharFounder, Ikiform

Add one extra text input that you hide off-screen, take out of the tab order and wrap in aria-hidden, with a name no browser autofills. People never fill it in; bots that fill every input do. On the server, treat any post with a value in it as spam and answer exactly as you would for a real message.

What's different about a honeypot

A honeypot has to be two things at once. To a bot reading the HTML, it must look like a normal field worth filling. To every person, it must not exist, and that includes people who don't use the form the way you do: someone on a screen reader, someone tabbing with a keyboard, someone whose password manager fills forms for them. Most honeypot bugs come from forgetting one of those people, and the symptom is a real message quietly filed as spam.

How a person and a bot meet the same honeypot fieldA person, with any browser or assistive techA script botfills theformscreen,keyboard orscreen readertrap isskippedoff-screen,tabindex -1,aria-hiddenautofillskips ita name nobrowserrecognisesreceivedtrap is emptyparses theHTMLfinds everyinputfills themallincluding thetrapposts atoncespamsame successanswerHow a person and a bot meet the same honeypot fieldA person, with any browser or assistive techA script botfills the formscreen, keyboard or screenreadertrap is skippedoff-screen, tabindex -1,aria-hiddenautofill skips ita name no browser recognisesreceivedtrap is emptyparses the HTMLfinds every inputfills them allincluding the trapposts at oncespamsame success answer
  1. A person fills in the form with whatever they use: a screen, a keyboard or a screen reader.
  2. The trap is off-screen, out of the tab order and hidden from assistive tech, so they never reach it.
  3. Its name is one browsers and password managers don't autofill, so it stays empty.
  4. The post arrives with an empty trap and is received.
  5. A script bot parses the HTML and finds every input, including the trap.
  6. It fills them all and posts at once.
  7. The trap has a value, so the post is spam. The bot still gets a normal success answer.
Each safeguard on the top row closes one way a real person could reach the field. Drop any of them and the top row can end in spam too.

The markup

Put the trap inside the form, next to the real fields, as a plain text input. A type="hidden" input doesn't work: bots know people can't edit those, and leave them alone.

contact.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
<style>
.hp {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
</style>
  • aria-hidden="true" on the wrapper removes the label and input from the accessibility tree, so screen readers don't announce them.
  • tabindex="-1" keeps the Tab key from landing on an input nobody can see.
  • The label says Leave this empty. If any tool still exposes the field, the person reading it knows what to do.
  • Don't add required. A required field that people can't see blocks every honest submit.

How to hide it

Bots have learned to skip fields that are obviously hidden. Simple scrapers still fill everything, but some check the hidden attribute or an inline display: none before they type. The further the hiding is from the input itself, the more bots fall for it.

TechniqueHidden from peopleBots it still catches
hidden attribute or inline display: noneYesOnly the simplest. It is the first thing a smarter bot checks
Off-screen class in your stylesheetYesMost. The bot must load and apply your CSS to tell
opacity: 0 or zero sizeMostly. Some magnifiers and focus outlines still reveal itMost, but riskier for people
Added by JavaScript after loadYesBots that run scripts. Plain HTML scrapers never see it, so it misses them

Use the off-screen class from your stylesheet. It catches the most bots and is the safest for people. If one honeypot stops working, add a second with a different name rather than a cleverer hiding trick.

Pick a name autofill ignores

Browsers and password managers fill fields by guessing from their name, id, label and autocomplete value. autocomplete="off" is a hint, and some browsers ignore it for fields that look like addresses or contact details. So the name does the real work.

AvoidWhyUse instead
email2, phone, name, addressAutofill recognises them and fills them for real people_gotcha, company_website
honeypot, trap, botSome bots skip fields with obvious namesA dull, plausible name
url, website aloneSome autofill tools fill a website field from a saved profileA compound name such as company_website

Check it on the server

A honeypot only works if the server reads it. A browser-side check is useless, since a bot never runs your script. When the field has a value, answer exactly as you would for a real message, and don't store or forward the post. A different answer tells the bot which field gave it away. The examples below call your own saveMessage; swap it for whatever stores or emails the message.

A plain HTML form has no server of its own. Point it at Ikiform and name the trap _gotcha, or rename it per form under Honeypot field. Ikiform does the server check, and the trap is never stored as an answer.

contact.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>
Log trapped posts, even if you don't store them. A count per day tells you whether the honeypot still earns its place, and a sudden drop to zero means bots have learned to skip it.

How Ikiform handles the honeypot

Every Ikiform form has a honeypot named _gotcha by default. Change it per form under Honeypot field in the form's settings; whatever name you pick is reserved and never stored as an answer. A filled honeypot adds 5 to the spam score, which reaches the threshold of 5 by itself.

Ikiform doesn't drop the post. It keeps it as spam, so if a real person ever trips the trap, you can find the message and restore it. A post judged as spam gets the same success answer as a real one, so a bot learns nothing. It is stored with the status spam, sends no notification email and no webhook, doesn't count toward your usage, and is deleted after 30 days. Every Monday, a workspace that blocked spam gets one summary email with the total, the busiest forms and the top reasons. The spam docs and HTML forms have the reference.

Check it works

  1. Load the form with a screen reader on and move through it. The trap is never announced.
  2. Tab through the form. Focus goes from the last real field straight to the button.
  3. Let your browser and password manager autofill the form. The trap stays empty.
  4. Post with the honeypot filled in, _gotcha=bot. It still gets a success answer, but the submission is stored as spam and sends no email or webhook.
  5. Send a normal message. It arrives as received, with no trace of the trap field.

Gotchas

  • JSON posts. If you build the body by hand instead of with FormData, include the trap, or your server reads it as empty for bots too.
  • Form builders that rename fields. Some builders prefix or rename inputs. Check the posted body, not the editor, for the trap's real name.
  • Caching. If a cached page still has an old trap name after you rename it, posts arrive with the old name and pass. Rename on both sides at once.
  • Checkbox honeypots. A hidden checkbox also works, but screen readers handle stray checkboxes worse than text inputs. Prefer a text input.
  • One layer. Bots that run a real browser and skip invisible fields walk past any honeypot. Pair it with a rate limit and, if needed, a human check.

Questions

Stuck on a step, or want a guide for your situation? Email hello@ikiform.com.