Guide
How to Add a Contact Form to a Static Website (No Backend Needed)
A static site is just files: HTML, CSS, maybe some JavaScript. That is what makes it fast and cheap to host, and it is also why a contact form doesn't work out of the box. A form has to send its data somewhere, and a folder of files has nowhere to send it.
The good news: you don't need to add a server. You need something that receives the submission and passes it on to you. This guide compares the four realistic options, then walks through the most common one with code you can paste, a JavaScript version, spam protection, the privacy bits most tutorials skip, and a checklist to make sure messages actually reach you.
Your four options at a glance
| Option | Setup | Good for | Watch out for |
|---|---|---|---|
| Form backend service | Change the form's action URL | Any host, any framework, any site builder | Free-plan limits, and where the provider stores data |
| Your host's built-in forms (e.g. Netlify Forms) | Add an attribute, redeploy | Sites that will stay on that host | Lock-in, and quirks with JavaScript-rendered forms |
| Your own serverless function | Write and deploy code, set up email sending | Developers who want full control | You now maintain spam filtering, email delivery and storage |
A mailto: link | One line of HTML | Almost nothing | Needs a configured mail app, exposes your address, no record kept |
For most people the first option is the right one: it works the same on GitHub Pages, Cloudflare Pages, Vercel, a Hugo or Astro build, or a page exported from a site builder, and moving hosts later doesn't break it. The rest of this guide uses that approach.
Step 1: the HTML form
Here is a complete, accessible contact form. Every field has a <label>, the email field uses type="email" so phones show the right keyboard, and there is a hidden spam trap we'll come back to.
<form action="https://formpit.com/f/YOUR_FORM_ID" 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="5" required></textarea>
<!-- Spam trap: people never see it, simple bots fill it in -->
<input type="text" name="_gotcha" tabindex="-1"
autocomplete="off" style="display:none">
<button type="submit">Send message</button>
</form>
Three details matter more than they look:
- Every input needs a
name. The browser only sends fields that have one. Forms that "submit but arrive empty" are nearly always missing names. method="POST"is required. Without it the browser uses GET and puts the message in the URL.- Name the email field
email. Most form services use it to set the reply-to address, so you can answer by pressing Reply.
Don't want to type it out? The contact form generator and the ready-made templates (plain HTML, Tailwind and Bootstrap) produce the same thing.
Step 2: point it at a form backend
Sign up with a form backend, create a form, and copy its endpoint URL into the form's action. With Formpit that looks like https://formpit.com/f/abc123. That's the whole integration: no SDK, no API key in your page, no build step.
When someone submits, the service stores the message, emails it to you, and shows the visitor a thank-you page. Most services let you redirect to your own thank-you page instead. In Formpit you can set that in the form's settings, or style Formpit's page to match your site's colours.
Switching from another service? Many form backends use similar hidden fields (_next or _redirect for the thank-you URL, _subject for the email subject, a honeypot such as _gotcha). Formpit accepts the common ones, so moving is usually a one-line change.
Step 3 (optional): submit without leaving the page
The plain form is the most reliable version and works even if JavaScript fails to load. If you'd rather show a success message in place, intercept the submit and send it with fetch:
<p id="form-status" role="status"></p>
<script>
const form = document.querySelector('form');
const status = document.getElementById('form-status');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const button = form.querySelector('button');
button.disabled = true;
try {
const res = await fetch(form.action, {
method: 'POST',
body: new FormData(form), // no Content-Type header!
headers: { Accept: 'application/json' },
});
if (!res.ok) throw new Error('HTTP ' + res.status);
form.reset();
status.textContent = 'Thanks! Your message was sent.';
} catch (err) {
status.textContent = 'Sorry, that didn\'t send. Please try again.';
} finally {
button.disabled = false;
}
});
</script>
Two things trip people up here. First, don't set a Content-Type header when the body is FormData: the browser has to set it itself so it can include the multipart boundary. Second, the service must allow cross-origin requests from your site. Formpit does; if another service doesn't, the request fails with a CORS error in the console even though the plain HTML form works.
Step 4: stop the spam before it starts
Any public form gets spam within days. You don't need a CAPTCHA to stop most of it. Layer a few cheap defences:
- A honeypot field like the
_gotchainput above. People never see it; simple bots fill in every field and get caught. - Server-side filtering for links, known spam phrases and gibberish. A good form backend does this for you and files suspicious messages in a spam folder instead of deleting them, so false positives can be recovered.
- A domain lock, so only submissions from your own site are accepted.
- A CAPTCHA only if needed. If a determined bot gets through, add Cloudflare Turnstile or hCaptcha, which usually pass real people without a puzzle.
We go deeper in how to stop contact form spam without a CAPTCHA, and you can test a message against Formpit's filter with the free spam protection tester.
Step 5: the privacy part most tutorials skip
A contact form collects personal data: at least a name and an email address. That comes with a few responsibilities, especially if you have visitors in the EU or UK.
- Collect only what you need. If you won't call people, don't ask for a phone number.
- Say who processes the data. Your privacy notice should mention that form submissions are handled by your form service, and roughly where it stores them. Check your provider's privacy policy and data processing terms.
- Don't keep messages forever by default. Delete old submissions you no longer need, or export and remove them periodically.
- Keep your own address out of the page. Services that put your email address in the form's HTML expose it to scrapers. An endpoint URL avoids that.
This isn't legal advice, but those four habits cover what most small sites need to think about.
Step 6: test it end to end before you rely on it
A form that looks fine can still lose messages. Before you link to your contact page, run through this list:
- Submit a real test from the live site, not from your editor's preview.
- Check it arrived in the service's inbox and in your email. If the email is missing, look in your spam folder and add the sender to your contacts.
- Press Reply on the notification email and confirm it addresses the visitor, not the service.
- Try it on a phone, with the keyboard covering half the screen.
- Submit with an empty required field and check the error is clear.
- If you use file uploads, send one and check it arrives as a file, not just a file name.
If something is off, paste your form's HTML into the form submission debugger. It flags missing names, wrong methods and file inputs without multipart, and shows exactly what will be sent. For more fixes, see 11 common reasons an HTML form doesn't work.
Notes for popular frameworks and builders
- Astro: with view transitions turned on, Astro can intercept form submissions. Add
data-astro-reloadto the<form>to let the browser submit it normally. - React, Vue, Svelte: controlled inputs still need a
nameif you submit withnew FormData(form). Without names, FormData is empty. - Next.js: a plain
<form action="https://…">posting to an external URL works as normal HTML. For an inline message, use thefetchpattern above in a client component. - Hugo, Jekyll, Eleventy: paste the HTML into a page or partial. Nothing else changes.
- Site builders (Framer, Webflow, Wix, Carrd and others): most let you embed custom HTML or set a custom form action. There are step-by-step guides for each platform.
Summary
Write a normal HTML form with names on every field and method="POST", set its action to a form backend, add a honeypot, mention the service in your privacy notice, and send yourself a real test. That's a working contact form on a static site in about ten minutes, with no server to look after.
Frequently asked questions
Can a static website have a contact form?
Yes. The page stays static; a form backend service receives the submission and emails it to you. You only set the form's action to the service's URL.
Is a mailto: link good enough?
Rarely. It depends on the visitor having a mail app set up, exposes your address to scrapers, and gives you no record or spam filtering.
Do I need JavaScript?
No. A plain HTML form with method="POST" works everywhere. JavaScript only adds an inline success message without leaving the page.
Is it GDPR friendly?
It can be. Collect only what you need, say in your privacy notice which service processes the form and where, and delete old submissions you no longer need.