How to Add a Contact Form to Any Static Website in 2 Minutes
Static sites are fast, secure, and cheap to host. But they cannot process form submissions on their own. Here is how to add a fully functional contact form to any static website without writing a single line of backend code.
The Static Site Form Problem
Static websites are HTML, CSS, and JavaScript files served directly from a CDN or file server. There is no server-side runtime (no Node.js, no PHP, no Python) to process form submissions. When a user fills out a contact form and clicks submit, the data has nowhere to go.
This is a problem because almost every website needs at least one form. Contact forms, newsletter signups, feedback forms, support requests — they all require somewhere to send the data. If you are building with static site generators like Hugo, Eleventy, Jekyll, Astro, or deploying a Next.js app with static export, you need a solution.
The good news is that this problem has been solved. There are three main approaches, and we will cover all of them so you can pick the one that fits your project best.
3 Ways to Handle Forms on Static Sites
Option 1: Build Your Own Backend
You can write a small API server (using Express, Flask, a serverless function, etc.) that receives form data, stores it in a database, and sends email notifications.
Pros:
- Full control over data handling and storage
- No third-party dependency
- Can implement custom business logic
Cons:
- Requires backend development skills
- You have to manage hosting, uptime, and scaling
- Need to implement email sending (SMTP, SendGrid, etc.)
- Need to build spam protection from scratch
- Ongoing maintenance and security updates
Time to implement: 2-8 hours depending on complexity
Best for: Projects with complex form processing requirements that no third-party service can handle.
Option 2: Generic Third-Party Services
Services like Google Forms, Typeform, or JotForm let you embed a form on your site. They handle everything from form rendering to data collection.
Pros:
- No coding required
- Built-in analytics and reporting
- Drag-and-drop form builders
Cons:
- Embedded forms look out of place (different styling)
- Limited control over the form's appearance
- Adds significant JavaScript and iframe overhead
- Users may not trust forms that look like third-party embeds
- Branding on free tiers ("Powered by Typeform")
Time to implement: 15-30 minutes
Best for: Non-technical users who do not care about visual consistency.
Option 3: Form Backend Service (Recommended)
A form backend service gives you an API endpoint URL. You build your own HTML form (with your own styling), but point it to the service's endpoint. The service handles data storage, email notifications, and spam filtering.
Pros:
- Complete control over form design and styling
- No backend code to write or maintain
- Built-in spam protection and email notifications
- Works with any static site generator or framework
- Setup takes less than 2 minutes
- Most services have a free tier
Cons:
- Third-party dependency for form processing
- Free tiers have submission limits
Time to implement: 2 minutes
Best for: Developers who want full design control with zero backend work. This is the approach we recommend for most static sites.
Tutorial: Add a Contact Form with FormCatch
Let us walk through the entire process of adding a contact form to a static website using FormCatch. We will cover plain HTML first, then show React and Hugo variations.
Step 1: Create a FormCatch Endpoint (30 seconds)
- Go to formcatch.vercel.app and sign up with your email.
- Click "Create Endpoint" on your dashboard.
- Give it a name (e.g., "Contact Form") and copy the endpoint URL.
Your endpoint URL will look like: https://formcatch.vercel.app/api/f/abc123
Step 2: Add the HTML Form (60 seconds)
Paste the following HTML into your static site. Replace the action URL with your actual endpoint URL.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contact Us</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px; }
h1 { margin-bottom: 24px; }
label { display: block; margin-bottom: 4px; font-weight: 600; }
input, textarea { width: 100%; padding: 10px; margin-bottom: 16px; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; }
textarea { height: 120px; resize: vertical; }
button { background: #2563eb; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer; }
button:hover { background: #1d4ed8; }
</style>
</head>
<body>
<h1>Contact Us</h1>
<form action="https://formcatch.vercel.app/api/f/your-form-id" method="POST">
<!-- Honeypot field for spam protection (hidden from users) -->
<input type="text" name="_gotcha" style="display:none" />
<label for="name">Name</label>
<input type="text" id="name" name="name" placeholder="Your name" required />
<label for="email">Email</label>
<input type="email" id="email" name="email" placeholder="you@example.com" required />
<label for="subject">Subject</label>
<input type="text" id="subject" name="subject" placeholder="What is this about?" />
<label for="message">Message</label>
<textarea id="message" name="message" placeholder="Your message..." required></textarea>
<button type="submit">Send Message</button>
</form>
</body>
</html>That is it. This is a complete, working contact form. When someone submits it, FormCatch will store the submission and send you an email notification with the form data.
Step 3: Test and Deploy (30 seconds)
Open the HTML file in your browser, fill out the form, and submit it. Check your email — you should receive a notification within seconds. Then deploy the file to your static hosting provider (Vercel, Netlify, Cloudflare Pages, GitHub Pages, or any CDN).
React / Next.js Version
If you are building with React or Next.js, you will want to handle form submission with JavaScript for a better user experience (no page redirect, inline success message, loading state).
'use client'
import { useState } from 'react'
const FORMCATCH_URL = 'https://formcatch.vercel.app/api/f/your-form-id'
export default function ContactForm() {
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setStatus('loading')
try {
const formData = new FormData(e.currentTarget)
const response = await fetch(FORMCATCH_URL, {
method: 'POST',
body: formData,
})
if (response.ok) {
setStatus('success')
e.currentTarget.reset()
} else {
setStatus('error')
}
} catch {
setStatus('error')
}
}
if (status === 'success') {
return (
<div className="p-6 bg-green-50 border border-green-200 rounded-lg text-center">
<h3 className="text-green-800 font-semibold text-lg">Message sent!</h3>
<p className="text-green-600 mt-1">We will get back to you soon.</p>
<button
onClick={() => setStatus('idle')}
className="mt-4 text-green-700 underline"
>
Send another message
</button>
</div>
)
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
{/* Honeypot */}
<input type="text" name="_gotcha" style={{ display: 'none' }} />
<div>
<label htmlFor="name" className="block font-medium mb-1">Name</label>
<input
type="text" id="name" name="name" required
className="w-full px-4 py-2 border rounded-lg"
placeholder="Your name"
/>
</div>
<div>
<label htmlFor="email" className="block font-medium mb-1">Email</label>
<input
type="email" id="email" name="email" required
className="w-full px-4 py-2 border rounded-lg"
placeholder="you@example.com"
/>
</div>
<div>
<label htmlFor="message" className="block font-medium mb-1">Message</label>
<textarea
id="message" name="message" rows={5} required
className="w-full px-4 py-2 border rounded-lg"
placeholder="Your message..."
/>
</div>
<button
type="submit"
disabled={status === 'loading'}
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{status === 'loading' ? 'Sending...' : 'Send Message'}
</button>
{status === 'error' && (
<p className="text-red-500 text-sm">Something went wrong. Please try again.</p>
)}
</form>
)
}This React component handles loading states, error states, success messages, and form reset. It uses FormData which automatically collects all named form fields, so you do not need to manage individual state variables for each input.
Hugo Version
Hugo is one of the most popular static site generators. Adding a FormCatch contact form to a Hugo site is straightforward. Create a partial template that you can reuse across pages.
<section class="contact-form">
<h2>Get in Touch</h2>
<form
action="https://formcatch.vercel.app/api/f/your-form-id"
method="POST"
class="form"
>
<!-- Spam protection -->
<input type="text" name="_gotcha" style="display:none" />
<!-- Optional: redirect after submission -->
<input type="hidden" name="_redirect" value="{{ .Site.BaseURL }}thank-you/" />
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea id="message" name="message" rows="6" required></textarea>
</div>
<button type="submit" class="btn-primary">Send Message</button>
</form>
</section>Then include the partial in any Hugo page template:
{{ define "main" }}
<div class="container">
<h1>{{ .Title }}</h1>
{{ .Content }}
{{ partial "contact-form.html" . }}
</div>
{{ end }}Advanced Features
The basic form above handles most use cases. But here are some additional features you can add to improve the experience.
Custom Redirect After Submission
By default, FormCatch shows a success page after submission. If you want to redirect users to your own thank-you page, add a hidden field:
<form action="https://formcatch.vercel.app/api/f/your-form-id" method="POST">
<input type="hidden" name="_redirect" value="https://yoursite.com/thank-you" />
<!-- ... form fields ... -->
</form>Custom Email Subject
You can set a custom subject line for the email notification by adding a _subject field:
<input type="hidden" name="_subject" value="New contact form submission" />AJAX Submission Without Page Redirect
For a smoother experience without a full page redirect, use JavaScript to submit the form in the background:
document.querySelector('form').addEventListener('submit', async (e) => {
e.preventDefault()
const form = e.target
const button = form.querySelector('button[type="submit"]')
button.textContent = 'Sending...'
button.disabled = true
try {
const response = await fetch(form.action, {
method: 'POST',
body: new FormData(form),
})
if (response.ok) {
form.innerHTML = '<p class="success">Message sent! We will be in touch.</p>'
} else {
throw new Error('Submission failed')
}
} catch (err) {
button.textContent = 'Send Message'
button.disabled = false
alert('Something went wrong. Please try again.')
}
})Which Static Site Generators Work with FormCatch?
FormCatch works with any static site generator or framework because it uses standard HTML forms and HTTP POST requests. Here is a non-exhaustive list of compatible tools:
- Hugo — use a partial template (shown above)
- Eleventy (11ty) — add the form to any Nunjucks, Liquid, or Markdown template
- Jekyll — include the form in any HTML or Markdown file
- Astro — use the form in any .astro component or page
- Next.js (static export) — use the React component shown above
- Gatsby — same React approach as Next.js
- Nuxt (static mode) — standard HTML form or Vue component
- SvelteKit (static adapter) — use in any Svelte component
- Docusaurus — add to any MDX or React page
- VitePress — embed in any Markdown or Vue page
- Plain HTML — the simplest option, no generator needed
Common Mistakes to Avoid
Here are the most common issues developers run into when adding forms to static sites, and how to avoid them:
- Forgetting the
nameattribute: Every form field must have anameattribute. Without it, the field's data is not included in the submission. This is the single most common form bug. - Using GET instead of POST: HTML forms default to GET, which puts form data in the URL. Always set
method="POST"for contact forms. - Missing CORS headers in AJAX: If you submit via fetch/AJAX and get CORS errors, make sure your form backend supports cross-origin requests. FormCatch handles CORS automatically.
- Not testing the honeypot: If you add a honeypot field, make sure it is actually hidden. Test that submissions work when the honeypot is empty (normal users) and get blocked when it is filled (bots).
- Hardcoding the endpoint URL: In frameworks like Next.js, use environment variables for the endpoint URL. This makes it easy to switch between development and production endpoints.
Frequently Asked Questions
Can a static website have a contact form?
Yes. Static websites can have fully functional contact forms by using a form backend service like FormCatch. The form submits data to an external API endpoint that handles storage, email notifications, and spam filtering. No server-side code is needed on your static site.
What is the easiest way to add a form to a static site?
The easiest way is to use a form backend service like FormCatch. Sign up, create an endpoint (takes 30 seconds), then add a standard HTML form with the endpoint URL as the action attribute. No JavaScript, no backend code, no server configuration needed.
How do static site forms handle spam?
Form backend services include built-in spam protection. FormCatch uses a honeypot technique where a hidden field catches bots that fill in every field. This approach requires no user interaction (unlike CAPTCHA), does not slow down your page, and blocks most automated spam.
Do I need to pay for a form backend for my static site?
Not necessarily. Most form backend services offer a free tier. FormCatch provides 1 endpoint and 50 submissions per month for free, which is enough for most personal and small business websites. You only need to upgrade if you have multiple forms or more than 50 submissions per month.
Summary
Adding a contact form to a static website does not require a backend. With a form backend service like FormCatch, you can have a fully functional contact form in under 2 minutes. The process is the same regardless of your static site generator: create an endpoint, add an HTML form, and deploy.
FormCatch handles the hard parts — data storage, email notifications, spam protection, and CORS — so you can focus on building your site. The free tier is generous enough for most personal and small business websites, and paid plans start at just $9/month when you need to scale.
Add a form to your static site in 2 minutes
Free tier includes 1 endpoint and 50 submissions/month.
Start Free — No Credit Card Required