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"andautocomplete="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”.requiredblocks 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.messagereads better three weeks later thanfield_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
/thankspage that sets expectations (“we reply within a day”) — it also gives your analytics a clean URL to count as a conversion. - Or submit with
fetchand 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.