How to add a contact form to a static site

By Nick Peplow ·

A contact form on a static site is two problems wearing one <form> tag. The markup is on you, and most copy-paste versions get the accessibility details wrong. The endpoint is the part your host won’t do — static hosting serves files and ignores POSTs. Both are solvable in an afternoon; here’s each one done properly.

What does the HTML actually need?

<form action="https://example.com/your-form-endpoint" method="POST">
  <label for="name">Name</label>
  <input id="name" name="name" type="text" autocomplete="name" required>

  <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" rows="6" required></textarea>

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

The details that earn their place:

  • Every label is bound with for/id. Clicking the label focuses the field, and screen readers announce what the field is for. Placeholder text is not a substitute — it disappears the moment someone starts typing, and assistive tech doesn’t treat it as a label.
  • type="email" gets the email keyboard on phones and format validation for free.
  • autocomplete="name" and autocomplete="email" let browsers fill the form in one tap. They also satisfy WCAG 2.1’s Identify Input Purpose criterion, which is the difference between “accessible” and “accessible on paper”.
  • required blocks empty submissions in the browser, before anything hits the network.
  • Every field has a name. Names become the keys in the email and the stored record. message reads better three weeks later than field_47.

What’s deliberately absent: wrapper divs and JavaScript. A plain POST works everywhere, and you can layer fetch on later without touching this markup.

How do you stop the spam?

Deploy a bare public form and the first bot submissions typically show up within days — that’s the pattern every form operator sees. The first line of defense is a honeypot: an extra field humans never see, which bulk bots fill in because they fill in everything.

<div style="position:absolute; left:-9999px" aria-hidden="true">
  <label for="website">Website</label>
  <input id="website" name="website" type="text" tabindex="-1" autocomplete="off">
</div>

Two rules make it work. First, the check must run server-side: a bot POSTs directly to your endpoint without ever rendering your page, so the only thing that matters is that the receiving end discards any submission where website is non-empty. If you built your own endpoint, that discard logic is yours to write. Second, hide the field by positioning rather than display:none or type="hidden" — the folk wisdom among form operators is that smarter bots skip fields hidden those two ways, and the off-screen version catches more.

A honeypot stops bulk bots, which is most of the volume. It doesn’t stop humans or headless browsers that execute your page. The right escalation is a captcha added when the honeypot stops being enough — not pre-emptively taxing every legitimate visitor with a puzzle on day one.

What happens after the visitor hits send?

By default the browser navigates to whatever the endpoint responds with — raw JSON or a generic hosted thank-you page, which is where visitors get stranded. Two fixes:

  • Redirect to your own page. Most form endpoints support a redirect target, usually via a hidden field. Build a real /thanks page that sets expectations (“we reply within a day”) — it also gives your analytics a clean URL to count as a conversion.
  • Or submit with fetch and show an inline confirmation, if the page already runs JavaScript. Keep the plain-POST markup as the fallback; it costs nothing.

Where does the submission data go?

This is the question most contact-form tutorials skip, and it’s the one that matters in month three. The answer depends on the endpoint:

  • Your own serverless function: wherever you coded it to go — which, in the common case, is “into the email API call and nowhere else”. If that call fails, the lead is gone.
  • Host-captured forms: into your hosting platform’s dashboard, coupled to that host.
  • A form backend: into the service’s inbox plus your email, independently of each other.

Whatever you pick, demand three things: the submission survives an email failure, there’s an export path, and retention is stated rather than implied.

Wiring it to a form backend

Using FormWire as the example — this is the entire integration contract:

<form action="https://api.formwire.com/submit" method="POST">
  <input type="hidden" name="access_key" value="YOUR-ACCESS-KEY">
  <input type="text" name="name" required>
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

Keep the labelled markup from the top of this page; the only lines that matter to the endpoint are the action URL and the hidden access_key. The key is safe to ship in public HTML because delivery is locked to email addresses the account owner has verified — the only thing anyone can do with it is send you a submission.

Three things come with the endpoint that you’d otherwise build yourself. The honeypot check runs server-side by default, so you don’t add the decoy field or write the discard logic — filtered spam is filed in its own tab, never emailed, and never counted against quota. The redirect is a hidden field: <input type="hidden" name="_redirect" value="https://yoursite.com/thanks">. And every submission is stored before the POST is acknowledged, then emailed with retries — so a delivery hiccup delays the notification instead of losing the lead. The free tier runs 250 submissions a month with unlimited forms, 30-day history, and CSV export.

The part worth being paranoid about

Most contact-form advice obsesses over markup and hand-waves the part where a stranger’s message has to survive spam filtering, email delivery, and your own need to find it three weeks later. The markup above is finished — copy it. The endpoint is the decision. Whichever you choose, put the honeypot in from day one, and when bots eventually get past it, escalate deliberately: FormWire’s spam protection documents how the honeypot, captcha, and domain-rule layers stack.