Log in

Contact form without a backend

A static site has no server by definition, which is exactly why contact forms are the first thing that pushes people off one. They do not have to be. The form stays in your HTML; only the sending happens somewhere else.

Live preview: this is the HTML and CSS below, rendered as-is.

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

Create your form
contact.html
<!-- The entire "backend". There is no second file. -->
<form action="https://submit.formpost.ai/YOUR_FORM_ID" method="POST">
  <h2>Contact us</h2>
  <p class="lede">Static site. No server. Still sends email.</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>

  <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" autocomplete="off">
  <input type="hidden" name="redirect" value="https://example.com/thank-you">

  <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;
}

What a backend was actually for

Three jobs, and only three: receive the POST, send an email, and store a copy. None of them are specific to your site — every contact form on the internet needs the same three things done the same way. That is what makes it worth handing over rather than rebuilding per project.

What you are not handing over is the form. The markup, the styling and the fields stay in your repository, rendered by your site generator, versioned with everything else. There is no widget, no iframe and no script tag.

The alternatives, honestly

ApproachWhat it costs you
A serverless functionA runtime to configure, a mail provider to sign up for, cold starts, and a deployment that can now fail
mailto: linkOnly works if the visitor has a desktop mail client set up. Most do not, and nothing visible happens
An embedded form widgetA third-party script, their styling, their branding, and a form that stops working when their CDN does
A form endpointOne attribute in your HTML

Works wherever static files work

GitHub Pages, Netlify, Vercel, Cloudflare Pages, S3, a folder on shared hosting, or a page builder that lets you paste raw HTML. Because the whole thing is one form element posting over HTTPS, there is no host-specific integration to install and nothing that only works on one platform.

The same is true of site generators — Hugo, Jekyll, Eleventy, Astro, Hexo. The form is markup in a template, so it is whatever your generator already understands.

The two lines worth adding before launch

The hidden honeypot catches most bots, and the redirect field sends visitors to your own thank-you page rather than ours. Both are in the markup above. Neither needs any code, and together they are the difference between a form that works and a form you are happy to leave up.

The endpoint being visible in your page source is fine — it can only deliver to the address its form was set up for. Turn on domain locking if you want to stop other sites posting to it.

Questions about this contact form

How do I add a contact form to a static site?
Write an ordinary HTML form and set its action to a form endpoint. There is nothing to deploy, no function to write and no database — the sending and the storage happen behind the endpoint.
Do I need a serverless function to handle form submissions?
No. A function would receive the POST and call a mail provider, which is exactly what the endpoint already does. Writing one gets you a runtime to maintain and a deployment that can fail.
Will this work on GitHub Pages?
Yes, and on Netlify, Vercel, Cloudflare Pages, S3 or any other static host. There is nothing running server-side on your end, so hosting restrictions never come into it.
Is it safe to have the endpoint in my page source?
Yes — it is designed to be public. An endpoint only delivers to the address its form was configured for, so copying it out of your HTML achieves nothing. Domain locking stops other origins posting to it.

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.