Static sites are fast, cheap, and easy to deploy. But they all hit the same wall: forms.
Without a backend, you can't process form submissions. No PHP, no Node server, no database. So how do you add a working contact form to a static site hosted on Vercel, Netlify, GitHub Pages, or anywhere else?
In this guide, I'll show you 3 approaches — from the simplest (2 minutes) to fully custom.
Option 1: HTML Form with a Form Backend (Easiest)
The fastest way. You point your HTML form to a third-party endpoint that handles everything — storing submissions, sending email notifications, and spam protection.
Here's a complete working example using SnapForm:
Step 1: Create a free account
Sign up at snapform.cc and create a new form. You'll get a unique endpoint URL.
Step 2: Add the form to your site
<form action="https://snapform.cc/api/f/YOUR_FORM_ID" method="POST">
<label for="name">Name</label>
<input type="text" name="name" id="name" required />
<label for="email">Email</label>
<input type="email" name="email" id="email" required />
<label for="message">Message</label>
<textarea name="message" id="message" required></textarea>
<!-- Honeypot spam protection (keep this hidden) -->
<input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off" />
<button type="submit">Send Message</button>
</form>
That's it. No JavaScript, no build step, no dependencies.
Step 3: Check your dashboard
Every submission shows up in your SnapForm dashboard with email notifications.
What you get for free:
- Email notifications
- File uploads (most services charge for this)
- Spam protection (honeypot, no CAPTCHA)
- CSV export
Option 2: AJAX Submission (For SPAs & Custom UX)
If you're using React, Vue, Astro, or any JavaScript framework, you probably want to handle the submission without a page redirect.
const form = document.querySelector('#contact-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const data = Object.fromEntries(formData);
try {
const res = await fetch('https://snapform.cc/api/f/YOUR_FORM_ID', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (res.ok) {
form.reset();
alert('Message sent!');
} else {
alert('Something went wrong. Please try again.');
}
} catch (err) {
alert('Network error. Please try again.');
}
});
This works from any domain — CORS is fully supported.
React Example
function ContactForm() {
const [status, setStatus] = useState('idle');
async function handleSubmit(e) {
e.preventDefault();
setStatus('sending');
const data = Object.fromEntries(new FormData(e.target));
const res = await fetch('https://snapform.cc/api/f/YOUR_FORM_ID', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
setStatus(res.ok ? 'sent' : 'error');
}
if (status === 'sent') return <p>Thanks! Message received.</p>;
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<button type="submit" disabled={status === 'sending'}>
{status === 'sending' ? 'Sending...' : 'Send'}
</button>
</form>
);
}
Option 3: With File Uploads
Need users to attach a resume, screenshot, or document? Just add enctype="multipart/form-data" and a file input:
<form action="https://snapform.cc/api/f/YOUR_FORM_ID"
method="POST" enctype="multipart/form-data">
<input type="text" name="name" placeholder="Name" required />
<input type="email" name="email" placeholder="Email" required />
<label for="resume">Attach your resume</label>
<input type="file" name="resume" id="resume" />
<input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off" />
<button type="submit">Submit</button>
</form>
Supported file types: JPEG, PNG, GIF, PDF, DOC/DOCX, XLS/XLSX, TXT, CSV, ZIP.
File upload with JavaScript:
const form = document.querySelector('form');
const formData = new FormData(form);
// Don't set Content-Type — the browser sets it automatically
const res = await fetch('https://snapform.cc/api/f/YOUR_FORM_ID', {
method: 'POST',
body: formData,
});
Tip: When uploading files via fetch, do NOT set the Content-Type header manually. The browser needs to set the multipart/form-data boundary automatically.
Adding Webhooks (Optional)
Want submissions sent to Slack, Zapier, or your own API? Add a webhook URL in your SnapForm dashboard. Every submission triggers a POST request:
{
"formId": "your-form-id",
"formName": "Contact Form",
"submissionId": "abc123",
"data": {
"name": "Jane Doe",
"email": "jane@example.com",
"message": "Hello!"
},
"createdAt": "2026-02-28T10:30:00.000Z"
}
Querying Submissions via API
If you're on a Pro or Business plan, you can query your data programmatically:
# List all forms
curl -H "Authorization: Bearer sk_live_xxx" \
https://snapform.cc/api/v1/forms
# Get submissions with pagination
curl -H "Authorization: Bearer sk_live_xxx" \
"https://snapform.cc/api/v1/forms/FORM_ID/submissions?page=1&limit=10"
Useful for building custom dashboards, syncing to a CRM, or exporting data on a schedule.
Quick Comparison: Form Backend Options
| Free Plan | Paid | File Uploads (Free) | |
|---|---|---|---|
| SnapForm | 50 subs/mo | $9/mo for 2,000 | Yes |
| Formspree | 50 subs/mo | $24/mo for 1,000 | No |
| Getform | 50 subs/mo | $16/mo for 1,000 | No |
| FormSubmit | Unlimited | Free | No |
Summary
For most static sites, Option 1 (plain HTML + form backend) is all you need. It works everywhere — GitHub Pages, Vercel, Netlify, Cloudflare Pages, or even a raw HTML file.
No backend. No JavaScript (unless you want it). No CAPTCHA. Just a form that works.
Links:
- SnapForm — Free signup, no credit card
- Documentation
- Full API Reference
What form solution are you using for your static sites? Let me know in the comments!
Top comments (0)