A wedding website is one of the nicest "first real projects" you can build: one page, a fixed audience, a hard deadline. The only part that seems to need a server is the RSVP form. It doesn't. This post shows two ways to collect RSVPs from a plain HTML page hosted for free on Netlify, GitHub Pages or Cloudflare Pages:
- Formspree – your form posts to a Formspree endpoint and responses land in your inbox/dashboard.
- Google Forms – responses go straight into a Google Sheet.
We'll also add a tiny countdown, because every wedding site needs one.
Note: free plans and their limits change. Check Formspree's and Google's current terms before relying on them for a big guest list.
1. The RSVP form (works without JavaScript)
Start with a normal HTML form. If JavaScript fails, the browser still submits it the old-fashioned way, so you never lose an RSVP.
<form class="rsvp" action="https://formspree.io/f/YOUR_FORM_ID" method="POST">
<input type="hidden" name="subject" value="New RSVP: Emma & Liam">
<!-- spam honeypot: keep hidden & empty -->
<input class="hp" type="text" name="_gotcha" tabindex="-1" autocomplete="off">
<label>Full name(s)
<input name="name" required autocomplete="name" placeholder="Your name">
</label>
<label>Email
<input type="email" name="email" autocomplete="email" placeholder="you@example.com">
</label>
<label>Attending?
<select name="attending">
<option>Joyfully accepts</option>
<option>Regretfully declines</option>
</select>
</label>
<label>Number of guests
<select name="guests"><option>1</option><option>2</option><option>3</option><option>4</option></select>
</label>
<label>Meal choice
<select name="meal"><option>Beef</option><option>Fish</option><option>Vegetarian</option></select>
</label>
<label>Dietary notes / song request
<textarea name="notes" rows="3"></textarea>
</label>
<button type="submit">Send RSVP</button>
<p class="form-status" role="status" aria-live="polite"></p>
</form>
.hp { position: absolute; left: -9999px; } /* hide the honeypot from humans */
A few details worth copying:
-
Every field has a
name. That's what shows up as the column/key in your responses. Noname, no data. -
_gotchahoneypot. Bots fill every field; humans never see this one. Formspree treats submissions with it filled as spam. -
role="status"+aria-live="polite"means screen readers announce your "Thank you!" message.
Setting up Formspree
- Create a free account at formspree.io and create a new form.
- Copy the endpoint, which looks like
https://formspree.io/f/abcdwxyz. - Replace
YOUR_FORM_IDin theactionattribute. - Submit once yourself – Formspree will ask you to confirm the first submission.
2. Submit with fetch and show a success message
Without JavaScript, the guest gets redirected to a Formspree thank-you page. That's fine, but staying on your page is nicer. Formspree returns JSON when you send Accept: application/json:
var form = document.querySelector('form.rsvp');
if (form) {
var status = form.querySelector('.form-status');
var button = form.querySelector('button[type="submit"]');
var say = function (msg) { if (status) status.textContent = msg; };
form.addEventListener('submit', function (e) {
// Guard: don't send anything while the placeholder ID is still there
if (form.getAttribute('action').indexOf('YOUR_FORM_ID') !== -1) {
e.preventDefault();
say('Preview mode: add your Formspree form ID to start receiving RSVPs.');
return;
}
if (!window.fetch || !window.FormData) return; // very old browser: normal post
e.preventDefault();
if (button) button.disabled = true;
say('Sending…');
fetch(form.action, {
method: 'POST',
body: new FormData(form),
headers: { 'Accept': 'application/json' }
})
.then(function (r) {
if (r.ok) { form.reset(); say('Thank you! Your RSVP has been sent.'); }
else { say('Sorry, your RSVP could not be sent. Please try again.'); }
})
.catch(function () {
say('Sorry, your RSVP could not be sent. Please check your connection and try again.');
})
.then(function () { if (button) button.disabled = false; });
});
}
Why it's written this way:
-
Progressive enhancement: if
fetchisn't available we simplyreturnand let the normal POST happen. - Disable the button while sending so nervous guests don't RSVP four times.
-
The final
.thenruns after success or failure, so the button always comes back. - The placeholder guard stops you from accidentally testing against a fake endpoint.
3. Alternative: Google Forms (responses in a Sheet)
If you'd rather have everything in a spreadsheet, Google Forms works too. You have two options.
Option A: just embed it
In Google Forms, click Send → < > and paste the iframe:
<iframe src="https://docs.google.com/forms/d/e/YOUR_FORM_KEY/viewform?embedded=true"
width="100%" height="900" frameborder="0">Loading…</iframe>
Easiest option, but it looks like Google Forms, not your site.
Option B: your own HTML, posting to Google Forms
- Build the form in Google Forms (Name, Attending, Guests, …).
- Click ⋮ → Get pre-filled link, type a dummy answer in each field, and copy the link. It looks like:
https://docs.google.com/forms/d/e/YOUR_FORM_KEY/viewform?usp=pp_url&entry.123456=Test&entry.789012=Yes
- Those
entry.NNNNvalues are your field names. ChangeviewformtoformResponseand use it as the form action:
<form id="gform" action="https://docs.google.com/forms/d/e/YOUR_FORM_KEY/formResponse"
method="POST" target="hidden_iframe">
<input name="entry.123456" required placeholder="Your name">
<select name="entry.789012">
<option>Yes</option><option>No</option> <!-- must match the form's options exactly -->
</select>
<button type="submit">Send RSVP</button>
<p id="gstatus" role="status" aria-live="polite"></p>
</form>
<iframe name="hidden_iframe" style="display:none"></iframe>
<script>
document.getElementById('gform').addEventListener('submit', function () {
document.getElementById('gstatus').textContent = 'Thank you! Your RSVP has been sent.';
});
</script>
Google doesn't allow cross-origin fetch reads of the response, so we post into a hidden iframe and show the message optimistically. Trade-off: you can't detect failures. Test it, then check the linked Sheet. Choice fields must match the option text exactly, and Google may change this undocumented behaviour, so Option A or Formspree is the safer choice if you want zero surprises.
Bonus: the pre-filled link is also handy on its own – you can send each guest a link with their name already filled in (&entry.123456=Aunt%20Rose).
4. A tiny countdown
<div class="countdown" id="countdown" data-date="2027-06-14T15:00:00"></div>
var cd = document.getElementById('countdown');
if (cd) {
var target = new Date(cd.getAttribute('data-date'));
var unit = function (n, one, many) {
return '<div><b>' + n + '</b><small>' + (n === 1 ? one : many) + '</small></div>';
};
var tick = function () {
if (isNaN(target.getTime())) { cd.textContent = ''; return false; } // bad date: hide
var t = target - new Date();
if (t <= 0) { cd.innerHTML = '<div><b>♥</b><small>Just married</small></div>'; return false; }
var d = Math.floor(t / 864e5), h = Math.floor(t / 36e5) % 24, m = Math.floor(t / 6e4) % 60;
cd.innerHTML = unit(d, 'Day', 'Days') + unit(h, 'Hour', 'Hours') + unit(m, 'Minute', 'Minutes');
return true;
};
if (tick()) { var timer = setInterval(function () { if (!tick()) clearInterval(timer); }, 30000); }
}
The date lives in a data- attribute, so non-developers can change it without touching JS. Minutes only (no seconds) means updating every 30 seconds is plenty, and the timer stops itself after the big day. Note the date string has no timezone, so it's interpreted in each visitor's local time – fine for a countdown, but add an offset (e.g. 2027-06-14T15:00:00+02:00) if you want it exact everywhere.
5. Deploy
Drag the folder onto Netlify Drop, push it to a GitHub Pages repo, or connect it to Cloudflare Pages. All three have free tiers for static sites. A custom domain is optional and costs extra (you pay the registrar).
That's it: a static page, a real RSVP flow, no backend, and files you own.
Disclosure: I made a set of one-page wedding website templates that use exactly this setup (Formspree or Google Forms RSVP, countdown, plain HTML/CSS/JS). They're paid, and they were produced with AI assistance. If you'd rather not build from scratch: Paper & Linen Studio on itch.io. Everything above works fine without them.
Top comments (0)