How to Add a Contact Form to a Static Website Without a Backend (2026)
You deployed a static site: a portfolio, a landing page, maybe a small product site. It's on GitHub Pages, Netlify, Cloudflare Pages, or just a folder of HTML files. Fast, cheap, no server to maintain. Then someone asks: "Does the site have a contact form?"
That question is harder to answer than it seems. Static files served from a CDN have no ability to receive, process, or store form submissions. The action attribute on your <form> tag has to point to something that actually runs code. Your CDN doesn't do that.
This post covers how to add a working, spam-filtered contact form to any static site, without writing backend code. Two approaches: a plain HTML form that requires zero JavaScript, and a vanilla JS version with loading and success states, also with no framework and no build step.
Why your static host can't process form submissions
When someone submits a form, the browser sends a POST request to whichever URL is in your form's action attribute. Something at that URL has to receive the request, parse the fields, store the data, and send you an email. That's server-side work.
A CDN serving static files isn't running code. It serves whatever file matches the URL path. A POST to a static host typically returns a 405 Method Not Allowed or just gets ignored.
Curious about the fuller picture of what "handling a form" actually involves at the server level? The hidden complexity of form handling walks through what's under the hood.
The solution is to point your form at an endpoint that does handle submissions. That endpoint lives somewhere else — not on your static host.
Your options
| Approach | What you maintain | Works on static hosting | Setup time |
|---|---|---|---|
| Your own server | A server, email infra, spam filtering | No | Days |
| Serverless function | A function, email API key, spam code | Yes | Hours |
| Form backend service | Nothing | Yes | 2 minutes |
For a static site, option 3 is almost always the right answer. You keep the benefits of static hosting (fast, cheap, no ops) and outsource the form-handling work to a service that specializes in it. This guide uses Formtorch.
If you want to understand why building your own form backend costs more than it looks, Why static sites still need a backend for forms covers that reasoning in depth.
The simplest version: plain HTML
This approach requires zero JavaScript and works on any host that can serve an HTML file. GitHub Pages, Hugo, Jekyll, Eleventy, a Cloudflare Pages deployment, a hand-edited index.html — all of them work.
<!-- contact.html -->
<form action="https://formtorch.com/f/YOUR_FORM_ID" method="POST">
<input
type="hidden"
name="_redirect"
value="https://yoursite.com/thank-you"
/>
<label for="name">Name</label>
<input id="name" name="name" type="text" required />
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
<button type="submit">Send message</button>
</form>When someone submits this form, the browser does a standard POST to Formtorch's servers. Formtorch stores the submission, runs spam checks, and sends you an email notification. Then it redirects to whatever URL you put in _redirect.
If you leave out _redirect, Formtorch shows its default thank-you page. Either way, your static host is never involved in the submission at all — the browser is talking directly to Formtorch's endpoint.
For a detailed look at HTML form mechanics and the full list of supported fields, see HTML contact forms that send email: a complete guide.
Better UX with vanilla JavaScript
The plain HTML version works, but it causes a full page navigation on submit. If you want to stay on the page, show a spinner, and swap in a success message without a redirect, you need a small amount of JavaScript.
The following example uses no framework, no npm packages, and no build step. It runs in any modern browser. Paste it into a plain .html file, a Hugo partial, a Jekyll _includes file, or an Eleventy template.
<!-- contact.html (or paste into any template) -->
<div id="contact-wrapper">
<form id="contact-form">
<div>
<label for="name">Name</label>
<input id="name" name="name" type="text" required />
</div>
<div>
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
</div>
<div>
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
</div>
<p id="form-error" hidden>Something went wrong. Please try again.</p>
<button type="submit" id="submit-btn">Send message</button>
</form>
<div id="success-message" hidden>
<p>Message sent. Thanks for reaching out — I'll get back to you soon.</p>
</div>
</div>
<script>
const form = document.getElementById("contact-form");
const successMessage = document.getElementById("success-message");
const errorMessage = document.getElementById("form-error");
const submitBtn = document.getElementById("submit-btn");
form.addEventListener("submit", async function (e) {
e.preventDefault();
submitBtn.disabled = true;
submitBtn.textContent = "Sending…";
errorMessage.hidden = true;
try {
const data = new FormData(form);
const res = await fetch("https://formtorch.com/f/YOUR_FORM_ID", {
method: "POST",
headers: { "X-Requested-With": "XMLHttpRequest" },
body: data,
});
if (!res.ok) throw new Error("Submission failed");
form.hidden = true;
successMessage.hidden = false;
} catch {
errorMessage.hidden = false;
submitBtn.disabled = false;
submitBtn.textContent = "Send message";
}
});
</script>A few things worth explaining here:
e.preventDefault(): stops the browser from doing its default form submission (the full-page navigation). You're taking over from here.
FormData(form): constructs the submission payload directly from the form element. You don't have to manually read each input — add or rename a field in the HTML and it just works, no JavaScript changes needed.
X-Requested-With: XMLHttpRequest: this header tells Formtorch that the request came from JavaScript, not a browser form submission. Without it, Formtorch responds with an HTML redirect instead of JSON, which isn't useful in a fetch handler.
hidden attribute toggle: instead of showing/hiding with CSS classes, toggling the hidden attribute is cleaner for semantic HTML. The success message starts hidden and the form hides when submission succeeds.
Re-enabling the button on error: if submission fails, the button goes back to its original state so the user can try again.
Does this work on your host?
Short answer: yes, on all of them. Here's why.
GitHub Pages: GitHub Pages serves static files. Your form submits to formtorch.com, which is an entirely different server. GitHub Pages never sees the POST request. It just serves the HTML file that contains the form.
Netlify: Netlify has its own built-in form handling, but it requires Netlify-specific attributes (netlify or data-netlify="true" on the form tag) and ties you to Netlify's submission processing. Formtorch works without any Netlify-specific configuration, and it's host-agnostic — if you ever move off Netlify, your form still works.
Cloudflare Pages: same as GitHub Pages and Netlify. The form renders from Cloudflare's edge, but the submission goes to Formtorch. Cloudflare Pages is not involved.
Vercel static export: if you're using output: 'export' in Next.js, your site deploys as static files. The same HTML form or fetch approach works without modification.
Hugo: paste the form HTML into a layout or partial file. Hugo outputs static HTML, so the runtime behavior is identical to a hand-authored HTML file.
Jekyll: same. Put the form in a _includes file or directly in a layout. The Liquid template engine processes it at build time and outputs plain HTML.
Eleventy: same. Add it to a Nunjucks, Liquid, or Handlebars template. The output is static HTML.
Spam protection
Formtorch runs TorchWarden on every submission by default. It scores each submission against a set of spam signals: hidden field filling (classic bot behavior), rapid-fire duplicate submissions, and pattern matching. Spam is stored separately in your dashboard and excluded from notifications. You don't configure anything.
If you're building a custom backend instead, a honeypot field is the most practical first line of defense. It's a hidden input that real users never interact with, but bots usually fill in:
<!-- Add inside your form. Hide with CSS, not type="hidden" -->
<input name="_honeypot" tabindex="-1" autocomplete="off" style="display:none" />For a deeper look at the spam landscape and which defenses hold up over time, see How to prevent spam in contact forms.
Where to get your form ID
Create a project
Sign in at formtorch.com and click New Project. Projects are containers for your forms.
Create a form
Inside your project, click New Form. Give it a name. The endpoint URL appears immediately.
Copy the endpoint
Your endpoint looks like https://formtorch.com/f/abc123. Replace
YOUR_FORM_ID in the examples above with the ID from that URL.
Set up notification emails
Go to Form Settings → Notifications and add your email. Confirm it, and you'll get an email for each new submission.
Wrapping up
- Plain HTML form: zero JavaScript, works anywhere that can serve an HTML file
- Vanilla JS fetch: loading and success states with no framework or build step
- Compatible with any static host: GitHub Pages, Netlify, Cloudflare Pages, Vercel static export, Hugo, Jekyll, Eleventy
- Spam filtering included with no configuration
You don't need a backend, a serverless function, or a hosting platform with built-in form support. A static HTML file and a form endpoint is enough.
Add a contact form to your static site today.
Create a free Formtorch account and get your endpoint URL in under two minutes.

