How to Add a Contact Form to a Static HTML Page (No Backend)

You built a page and it needs a "get in touch" box. The HTML for a form is easy; the problem is what happens when someone presses Submit. A static host serves files — it doesn't run code, so there's nothing to receive the submission. This guide covers the four ways around that, with code you can paste, and the spam problem that shows up about a week later.

Why the obvious version does nothing

This is what most people write first:

<form action="#" method="post">
  <input name="email">
  <button>Send</button>
</form>

Pressing Send reloads the page and discards everything typed. There's no error — which is why it's easy to ship a contact form that has quietly been a decoration for months. A form needs an action pointing at something that accepts a POST. Your job is to supply that something without running a server.

Option 1: A form-handling service (the usual answer)

Formspree, Web3Forms, Getform, Basin and others give you an endpoint URL. Point your form at it; they receive the submission and email it to you. Free tiers typically cover 50–250 submissions a month, which is plenty for a personal site.

<form action="https://formspree.io/f/YOUR_FORM_ID" method="POST">
  <label>Your email
    <input type="email" name="email" required>
  </label>

  <label>Message
    <textarea name="message" rows="5" required></textarea>
  </label>

  <!-- honeypot: bots fill it, humans never see it -->
  <input type="text" name="_gotcha" tabindex="-1" autocomplete="off"
         style="position:absolute;left:-9999px">

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

That's a working contact form on a static page — no JavaScript required, and it degrades gracefully because it's a plain HTML form.

Submitting without leaving the page

By default the browser navigates to the service's thank-you page. To keep people on your site, submit with fetch:

<script>
document.querySelector('form').addEventListener('submit', async function (e) {
  e.preventDefault();
  var form = e.target;
  var status = document.getElementById('status');
  status.textContent = 'Sending...';
  try {
    var res = await fetch(form.action, {
      method: 'POST',
      body: new FormData(form),
      headers: { Accept: 'application/json' }
    });
    if (!res.ok) throw new Error('Request failed');
    form.reset();
    status.textContent = 'Thanks — I will get back to you.';
  } catch (err) {
    status.textContent = 'Something went wrong. Email me at hi@example.com instead.';
  }
});
</script>

Note the fallback in the error branch. Third-party endpoints do go down, and a form that fails silently is worse than no form.

Option 2: Google Forms or Airtable, embedded

Free, unlimited submissions, and the responses land in a spreadsheet you already know how to use. The trade-off is visual: an embedded iframe rarely matches your page's design, and it adds a heavy third-party frame to the page.

<iframe src="https://docs.google.com/forms/d/e/YOUR_FORM_ID/viewform?embedded=true"
        width="100%" height="700" frameborder="0" title="Contact form">
</iframe>

Good for a survey or an internal signup sheet. Poor for a designed landing page.

Option 3: No form at all

Worth considering seriously, because for many pages a form is ceremony around an email address.

<a href="mailto:hi@example.com?subject=Hello%20from%20your%20site">
  hi@example.com
</a>

It works everywhere, has nothing to break, and costs nothing. The downsides: the address is scrapeable by spam crawlers, and visitors on a device without a configured mail client get a confusing prompt. For a portfolio, a plain visible email address is often better than a form — people can copy it.

Option 4: Your own serverless function

If you want full control — custom validation, storing to a database, posting to Slack — a single serverless function (Cloudflare Workers, Netlify Functions, Vercel Functions, AWS Lambda) does it, and free tiers are generous.

This is no longer "no backend", it's "a very small backend", and you take on CORS configuration, secret management and the deploy pipeline that comes with it. Choose it when a service genuinely can't do what you need, not by default.

Spam, which will find you

A public form gets found by bots within days. In rough order of effort:

  • Honeypot field — the hidden input shown above. Free, invisible to humans, and catches a large share of naive bots. Every decent form service supports one.
  • Service-side filtering — most form providers include spam filtering on paid tiers and some on free.
  • A CAPTCHA — hCaptcha, Turnstile or reCAPTCHA. Effective, but it costs your visitors friction and adds a third-party script. Add it when spam actually becomes a problem, not before.
  • A time check — reject submissions completed in under two seconds. Humans are slower than scripts.

Making the form accessible and pleasant

Small things that materially improve completion rates:

  • Every input needs a real <label>. Placeholder text is not a label — it vanishes when typing starts and screen readers may skip it.
  • Use the right type (email, tel, url). Phones show a better keyboard and the browser validates for free.
  • Add autocomplete attributes (name, email) so browsers can fill fields in.
  • Keep it short. Every optional field costs you submissions; ask for what you'll actually use.
  • Announce success and failure in text, not only colour.

Which to choose

ApproachSetupMatches your designCostBest for
Form service5 minutesYesFree tier, then paidMost sites
Embedded Google Form5 minutesNoFreeSurveys, signups
mailto: link1 minuteYesFreePortfolios
Serverless functionAn hour+YesFree tierCustom logic

One hosting note: if your site is a single file — as it is on hdply — all of this still works, because the form markup, styles and script live in the same HTML. What you can't do is point the form at a script on your own server, since there isn't one.

Common questions

Can I store submissions myself without a backend? Only in the visitor's own browser (localStorage), which is useless for receiving messages. Anything that reaches you crosses a network to someone's server — the question is only whose.

Is my form service seeing my messages? Yes. If people will send sensitive information, read the provider's data-handling terms and pick accordingly.

Why do submissions land in spam? Because the notification is sent by the service, not by your domain. Whitelist the sender, and set the reply-to to the submitter's address so replying works.

Related: Build a one-page portfolio · Deploy without a server

From a file on your desktop to a live URL in under a minute.

Deploy your first page