Log in

Contact form with captcha

Two layers, in the order you should add them. The honeypot is invisible, costs nothing and stops most automated submissions on its own. The captcha goes on top for the ones that get past it.

Where the widget goes. The preview does not load Turnstile — the dashed box marks the div the script renders into.

Replace YOUR_FORM_ID with a form of your own and this sends to your inbox.

Create your form
contact.html
<form action="https://submit.formpost.ai/YOUR_FORM_ID" method="POST">
  <h2>Contact us</h2>
  <p class="lede">We read every message. Bots do not get through.</p>

  <label for="name">Name</label>
  <input id="name" type="text" name="name" required>

  <label for="email">Email</label>
  <input id="email" type="email" name="email" required>

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="3" required></textarea>

  <!-- Layer one: invisible to people, irresistible to bots -->
  <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" autocomplete="off">

  <!-- Layer two: the widget renders itself into this div -->
  <div class="cf-turnstile" data-sitekey="YOUR_TURNSTILE_SITE_KEY"></div>
  <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

  <button type="submit">Send message</button>
</form>
contact.css
body {
  margin: 0;
  padding: 32px 16px;
  background: #fafafa;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  color: #171717;
}

form {
  display: flex;
  flex-direction: column;
  max-width: 430px;
  margin: 0 auto;
  padding: 28px;
  background: #fff;
  border: 1px solid #ededed;
  border-radius: 14px;
}

h2 {
  margin: 0 0 4px;
  font-size: 20px;
}

.lede {
  margin: 0 0 20px;
  font-size: 14px;
  color: #737373;
}

label {
  margin-bottom: 6px;
  font-size: 13px;
  font-weight: 600;
  color: #404040;
}

input,
textarea {
  box-sizing: border-box;
  font: inherit;
  margin-bottom: 16px;
  padding: 10px 12px;
  color: #171717;
  background: #fff;
  border: 1px solid #d4d4d4;
  border-radius: 8px;
}

input:focus,
textarea:focus {
  border-color: #171717;
  outline: 2px solid rgba(23, 23, 23, 0.12);
}

textarea {
  resize: vertical;
}

button {
  padding: 11px 16px;
  font: inherit;
  font-weight: 600;
  color: #fff;
  background: #171717;
  border: 0;
  border-radius: 8px;
  cursor: pointer;
}

.hint {
  margin: -8px 0 16px;
  font-size: 12px;
  color: #a3a3a3;
}

Start with the honeypot

A field people never see and bots fill in anyway, because most of them populate every input they find. Anything arriving with it filled is discarded. It costs one line, it never asks a real visitor to do anything, and it removes the bulk of automated spam before you have made anyone look at a puzzle.

<input type="checkbox" name="botcheck" style="display:none" tabindex="-1" autocomplete="off">

The tabindex and autocomplete attributes matter: without them a keyboard user can tab into an invisible field, and a password manager can fill it. Both would flag a genuine visitor as a bot.

Then add the captcha

Turnstile, hCaptcha and reCAPTCHA all work the same way here — render the widget as their documentation describes, then save the matching secret key in your form settings. The token is verified server-side before the submission is accepted, which is the part that makes it worth anything: a captcha checked only in the browser stops nobody.

  • Turnstile is usually the right default — no puzzles for most visitors, and no visual challenge unless something looks wrong.
  • hCaptcha if you need a privacy-focused provider with a free tier and are willing to show interactive challenges.
  • reCAPTCHA if you are already invested in it. Be aware of what it means for your privacy policy in the EU.
  • Whichever you pick, the secret key goes in your form settings and never into your markup. Only the site key is public.

Do not reach for a captcha first

Every captcha costs you real submissions. People abandon forms over them, some fail them repeatedly through no fault of their own, and screen reader users have the worst time of anyone. Add the honeypot, launch, and see whether you have a problem before you make every visitor prove they are human.

Blocked submissions never count toward your monthly quota, so a bot cannot exhaust it on purpose.

Questions about this contact form

Which captcha should I use on a contact form?
Cloudflare Turnstile for most sites — it is free, usually invisible, and does not show a puzzle unless something looks suspicious. hCaptcha and reCAPTCHA are both supported if you prefer them.
Do I need a captcha at all?
Probably not on day one. A honeypot field stops the majority of automated submissions and costs a real visitor nothing. Add a captcha when you can see spam arriving, not before.
Where does the secret key go?
In your form settings, never in your HTML. Only the site key belongs in the markup; the secret is what proves the token is genuine when it is verified server-side.
Does the honeypot field hurt accessibility?
Not if you set tabindex="-1" and autocomplete="off" as shown. Without those a keyboard user can tab into it or a password manager can fill it, and a genuine visitor gets treated as a bot.

Related examples

Or go back to all 21 contact form examples.

Give this form an endpoint

Formpost takes the submission, emails it to you and keeps a searchable copy. Free for 250 messages a month, with unlimited forms and no card.