splitforms.com
All articles/ TUTORIALS9 MIN READPublished May 1, 2026

How to add a contact form to a static site (no backend)

Step-by-step guide to add a working contact form to any static HTML site without writing server code. Three methods compared, with copy-paste examples.

✶ Written by
splitforms.com / blog

Founder of splitforms — the form backend API for developers. Writes about form UX, anti-spam, and shipping web apps without backend code.

Why static sites need outside help for forms

A static site is just HTML, CSS, and maybe a sprinkle of JavaScript served by a CDN. There's no Node, no PHP, no database — which is exactly why static sites are fast, cheap, and hard to attack. But it also means there is nothing on the server to receive a form POST. The browser sends the data somewhere, and that somewhere has to be a real backend.

You have three honest options: pay a form backend service to receive the POST, run a tiny serverless function in front of your static site, or fall back to mailto: and accept the UX hit. The rest of this guide walks through each, with copy-pasteable HTML you can ship today.

The three real options in 2026

Setting aside the legacy stuff (PHP mail(), Google Forms iframe hacks, Formtools self-hosted on a VPS you'll forget to patch), three patterns actually work in production:

  • Hosted form backend — services like splitforms, Formspree, Web3Forms, Basin, or Netlify Forms. Drop a URL into action, done.
  • Serverless function — a tiny Vercel, Netlify, or Cloudflare Worker that accepts the POST and forwards via Resend, Postmark, or Mailgun.
  • mailto: link— no backend at all; opens the visitor's mail client.

Method 1: Form backend (recommended)

This is the path of least resistance. A form backend exposes a single POST endpoint. You point your form at it, you get email. Setup takes about 90 seconds.

Here's a complete, working contact form using splitforms:

<form action="https://splitforms.com/api/submit" method="POST">
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
  <input type="hidden" name="redirect" value="https://yoursite.com/thanks" />
  <input type="hidden" name="subject" value="New lead from yoursite.com" />

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

  <!-- honeypot: bots fill it, humans don't see it -->
  <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" autocomplete="off" />

  <button type="submit">Send</button>
</form>

That's the entire integration. No fetch, no JSON, no CORS headache, no DNS records. The browser POSTs the form, splitforms receives it, runs spam filtering, emails you the contents, and 302-redirects the visitor to your /thanks page.

If you want to stay on the same page (single-page-app style), use fetch() instead:

<form id="contact">
  <input name="name" required />
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

<script>
document.getElementById('contact').addEventListener('submit', async (e) => {
  e.preventDefault();
  const formData = new FormData(e.target);
  formData.append('access_key', 'YOUR_ACCESS_KEY');

  const res = await fetch('https://splitforms.com/api/submit', {
    method: 'POST',
    body: formData,
  });

  if (res.ok) {
    e.target.innerHTML = '<p>Thanks — we\'ll be in touch.</p>';
  } else {
    alert('Something went wrong. Please email us directly.');
  }
});
</script>

splitforms returns CORS headers by default, so the fetch works from any origin. The same code runs on Netlify, Vercel, Cloudflare Pages, GitHub Pages, S3 + CloudFront, or a USB stick served from a Raspberry Pi.

Method 2: Serverless function

If you want to own the email-sending pipeline yourself — maybe to add custom logic, store submissions in a database, or trigger a Slack message — a tiny serverless function works well. The tradeoff: you now own deliverability, retries, spam filtering, and uptime.

Here's a Cloudflare Worker version using Resend:

// worker.js
export default {
  async fetch(request, env) {
    if (request.method !== 'POST') return new Response('Nope', { status: 405 });

    const data = await request.formData();
    const name = data.get('name');
    const email = data.get('email');
    const message = data.get('message');

    // honeypot
    if (data.get('botcheck')) return new Response('OK'); // silently drop

    const r = await fetch('https://api.resend.com/emails', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer ' + env.RESEND_API_KEY,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        from: 'forms@yourdomain.com',
        to: 'you@yourdomain.com',
        subject: 'New contact form submission',
        text: `From: ${name} <${email}>\n\n${message}`,
      }),
    });

    if (!r.ok) return new Response('Email send failed', { status: 500 });

    return Response.redirect('https://yoursite.com/thanks', 303);
  },
};

Then point your form at https://your-worker.workers.devinstead of splitforms. You'll need a verified sending domain in Resend (SPF + DKIM records) or your emails will land in spam — see receive form submissions by email for the deliverability checklist.

Method 3: mailto: link

The simplest option. No backend at all:

<a href="mailto:you@yoursite.com?subject=Inquiry&body=Hi%20there">
  Email me
</a>

You can technically do this with a <form> too:

<form action="mailto:you@yoursite.com" method="POST" enctype="text/plain">
  <input name="name" />
  <textarea name="message"></textarea>
  <button type="submit">Send</button>
</form>

Both rely on the visitor having a configured email client. Mobile users on Gmail-only setups, anyone using a shared computer, and most corporate users will get a broken experience. Industry data from form analytics providers consistently shows mailto: converts 3–10x worse than a real form. Use it as a fallback, not your primary CTA.

Side-by-side comparison

MethodSetup timeCost (small site)Spam protectionMobile UX
splitforms / form backend~90 seconds$0 (free tier)Built inExcellent
Serverless + Resend/Postmark30–60 min$0–5/moYou build itExcellent
mailto:30 seconds$0None neededPoor

For 95% of marketing sites, portfolios, landing pages, and docs sites, the form backend wins on every axis except "I want to own everything."

Spam protection without breaking UX

Static-site contact forms get scraped fast. Within a week of going live, expect 5–50 bot submissions a day. Three layers stop ~99% of them without a CAPTCHA in front of human visitors:

  1. Honeypot field — a hidden input named something like botcheck or website. Bots fill every input they see; if it's populated, drop the submission silently.
  2. Time-to-submit check — humans take 5+ seconds to fill a form; bots take milliseconds. splitforms tracks this automatically.
  3. Rate limiting per IP and per access key— caps obvious flood attacks. Built into splitforms; in a serverless function you'd use Cloudflare KV or Upstash Redis.

If those three aren't enough (you'll know — your inbox will tell you), bolt on Cloudflare Turnstile. It's free, GDPR-clean, and passes ~99% of humans without showing a challenge.

Deploying on Netlify, Vercel, Cloudflare Pages, GitHub Pages, S3

The HTML above runs unchanged on every static host. A few host-specific notes:

  • Netlify— has its own free Forms feature (100 submissions/mo, then $19/mo for 1,000). splitforms' free tier gives you 5x the volume at the same price.
  • Vercel — no native form handling. Use a form backend or write a Server Action / Route Handler.
  • Cloudflare Pages — same as Vercel. Pair with Workers if you go the serverless route.
  • GitHub Pages — pure static, no functions. Hosted form backend is your only realistic option.
  • S3 + CloudFront — same constraint. Hosted backend or API Gateway + Lambda.

Tech support and troubleshooting

The most common static-site form failures and the one-line fix:

  • Form posts but no email arrives. Search the spam folder for the splitforms domain. If nothing is there, open the dashboard's Submissions tab — if you see the row, the failure is on the email leg (DMARC, full inbox, blocked sender). Otherwise the form HTML is wrong.
  • Browser shows a raw JSON response after submit. Add <input type="hidden" name="redirect" value="https://yoursite.com/thanks" /> so the browser follows a 302 to your thank-you page instead of rendering the API response.
  • CORS error in the console when using fetch(). The endpoint URL is wrong. Confirm it is exactly https://splitforms.com/api/submit. splitforms returns CORS headers for any origin from that endpoint.
  • Submissions are getting blocked as spam. Check the dashboard for the spam reason — usually the honeypot tripped because something on the page auto-fills hidden inputs (Bitwarden, LastPass). Rename the honeypot input from botcheck to website_url or any uncommon name.
  • Localhost tests get 403. You enabled the "Allowed domains" restriction in your dashboard. Either disable it during development or include localhost in the allow-list.

The full failure-mode catalogue lives in contact form not working.

Where to go next

Frequently asked questions

What is the easiest way to add a contact form to a static HTML site?

Point your form's action attribute at a hosted form backend like splitforms (https://splitforms.com/api/submit) and add a hidden input with your access key. The backend receives the POST, emails you the submission, and redirects the visitor to a thank-you page. No server, no JavaScript required.

Do I need JavaScript for a static site contact form?

No. A plain HTML <form> with method="POST" and action set to a form backend URL works without any JavaScript. JavaScript is only needed if you want to submit via fetch() and stay on the same page, or to add client-side validation.

Can I use mailto: links instead of a form backend?

You can, but mailto: opens the visitor's local email client, which most users on mobile or shared computers don't have configured. Industry conversion data shows mailto links convert 3-10x worse than a real form. Use a form backend or serverless function instead.

Is it safe to put my access key in HTML?

Yes, with one caveat. Form backend access keys are designed to be public (they're like Stripe publishable keys). Lock the key to your domain in the splitforms dashboard so other sites can't use it to send spam from your account.

How do I prevent spam on a static site contact form?

Add a honeypot field (a hidden input bots fill in but humans don't) and rely on your form backend's built-in spam filtering. splitforms uses honeypot detection plus heuristic scoring out of the box. Add Cloudflare Turnstile if you want a CAPTCHA fallback.

Will a static-site form work without HTTPS?

The form will technically POST, but every modern browser flags non-HTTPS forms as insecure and most form backends (splitforms included) reject HTTP-origin requests. Static hosts like Netlify, Vercel, Cloudflare Pages, and GitHub Pages give you free HTTPS — there is no reason to skip it.

Can I send the same submission to multiple destinations from a static site?

Yes. Configure a webhook on the splitforms form pointing at Slack, Discord, Zapier, or your own endpoint, and submissions fan out automatically while still landing in your inbox. The static HTML stays a one-form-tag setup.

Where can I get help if my static site form is broken?

Read /faq for common deliverability and access-key issues, /docs for the request contract, and /api-reference for the full endpoint spec. The blog post 'contact form not working' walks through the eight failure modes splitforms support sees most often.

About the author
✻ ✻ ✻

Get your free contact form API key in 60 seconds.

1,000 free form submissions per month. No credit card. No SDK, no PHP, no plugin. Drop one POST endpoint in your form and submissions land in your inbox.

Generate access key →Read the docs
v0.1 · founders pricing locked in · early access open