Guide
Contact form for a static site, no backend needed
A static site has no server to receive a form POST, which is why so many static sites ship with a dead contact form or a bare mailto: link. The fix is one attribute: point the form's action at an endpoint that stores and emails the submission for you.
Step 1: Create your endpoint
Sign up, create a form, and copy your endpoint. Add and verify the email address you want submissions delivered to. That is the whole backend, and you never host it.
Step 2: A complete working page
Here is an entire contact page, ready to save as contact.html. Replace YOUR_FORM_ID and add your own styling:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contact</title>
</head>
<body>
<h1>Contact</h1>
<form action="https://forms.scrolltheory.media/f/YOUR_FORM_ID" method="POST">
<label>Name
<input type="text" name="name" required>
</label>
<label>Email
<input type="email" name="email" required>
</label>
<label>Message
<textarea name="message" rows="5" required></textarea>
</label>
<!-- hidden spam honeypot: leave empty -->
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off"
style="position:absolute;left:-9999px" aria-hidden="true">
<button type="submit">Send message</button>
</form>
</body>
</html>
Upload it wherever your site lives: GitHub Pages, Cloudflare Pages, an S3 bucket, shared hosting, anything that serves HTML. There is nothing to install and no dependency to update.
Step 3: Send a test
Open the page, fill it in, and submit. The visitor sees a clean thank-you page, the submission shows up in your dashboard, and a notification lands in your inbox with reply-to set to the sender. Add a hidden _next field if you want the visitor sent to your own thank-you page instead.
Optional: stay on the page
The form above works with zero JavaScript. If you would rather show a success message without leaving the page, add this below the form:
<script>
document.querySelector("form").addEventListener("submit", async (e) => {
e.preventDefault();
const form = e.currentTarget;
const res = await fetch(form.action, {
method: "POST",
headers: { "Accept": "application/json" },
body: new FormData(form)
});
const data = await res.json();
form.outerHTML = data.ok
? "<p>Thanks, your message was sent.</p>"
: "<p>Something went wrong. Please try again.</p>";
});
</script>
What about spam?
The hidden honeypot field quarantines most bots on its own. Flagged messages are held in a separate view instead of deleted, so a wrongly flagged real message is one click from recovery, and spam never counts against your monthly quota.
Get your endpoint
Create a form on the free plan and you get an endpoint to drop into any of the examples above. Every submission is stored and spam is quarantined rather than deleted, so a false positive never costs you a real lead.