SAVEFORM
All posts
Guide

The 4 ways to handle form-submit success on a static site

Redirects vs success pages vs JS-driven UI vs progressive enhancement: when to reach for which, with the trade-offs nobody mentions.

4 min readforms·ux·static-sites

The submit button gets clicked. Now what? It's a tiny moment, but it shapes the whole experience. A janky transition reads as “something went wrong” even when the submission worked perfectly. There are four good ways to handle it. Here's when to use which.

Option 1: Native browser, default success page

The simplest possible thing. The form posts, the server returns an HTML page that says “thanks”, the browser navigates to it. No JavaScript, no fetch, no error states.

HTMLcontact.html
<form action="https://your-endpoint" method="POST">
  <input name="email" />
  <button>Submit</button>
</form>

Use it when: you don't have analytics to fire on submit, you don't need a custom thank-you page, and you don't mind the visual jolt of a full-page navigation.

Skip it when: the rest of your site is a slick SPA. The abrupt server-rendered success page feels like a design regression.

Option 2: Native browser, redirect to your own page

Same as option 1, but you tell the form backend where to send the user after success. Most services support this either via a hidden _redirect field or a settings toggle.

HTMLcontact.html
<form action="https://your-endpoint" method="POST">
  <input type="hidden" name="_redirect" value="https://yoursite.com/thanks" />
  <input name="email" />
  <button>Submit</button>
</form>

Use it when: you want a thank-you page that matches your design, fires your analytics conversion event, and maybe upsells the next thing.

Option 3: JavaScript fetch, in-place success UI

For SPAs and anywhere the page navigation feels heavy. You intercept the submit, post via fetch, and swap the form out for a thank-you message inline.

JavaScriptcontact.js
form.addEventListener('submit', async (e) => {
  e.preventDefault();
  setSubmitting(true);

  try {
    const res = await fetch('https://your-endpoint', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
      body: JSON.stringify(Object.fromEntries(new FormData(form))),
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    showThanks();
  } catch (err) {
    showError(err);
  } finally {
    setSubmitting(false);
  }
});

Use it when: you want fine-grained control over loading, success, and error states, and your audience has JavaScript enabled (which is essentially all of them, but see option 4 below).

The error states people forget

Three error states matter and they get treated differently:

Option 4: Progressive enhancement

The technically-best version. The form works as a plain HTML form if JavaScript fails to load, and upgrades to in-place fetch when JS is present. Two paths, one piece of HTML.

HTMLcontact.html
<form id="contact" action="https://your-endpoint" method="POST">
  <!-- Used by the no-JS path -->
  <input type="hidden" name="_redirect" value="/thanks" />

  <input name="email" required />
  <button>Submit</button>
</form>

<script type="module">
  // Used by the JS path. preventDefault hijacks the native submit
  const form = document.getElementById('contact');
  form.addEventListener('submit', async (e) => {
    e.preventDefault();
    const res = await fetch(form.action, {
      method: 'POST',
      headers: { Accept: 'application/json' },
      body: new FormData(form),
    });
    if (res.ok) form.outerHTML = '<p>Thanks, we&rsquo;ll be in touch.</p>';
  });
</script>

Use it when: the form is high-stakes (signups, payments, lead capture for a paid product) and you want the maximum-resilience version that survives flaky networks, ad blockers, and broken JavaScript.

Picking, in one paragraph

If your site is server-rendered and a full navigation feels native, use option 2 (redirect to your own thank-you page). If your site is a SPA, use option 3 (fetch + in-place UI). If conversions on this form genuinely matter to your business, write the extra ten lines for option 4. Save option 1 (the default success page) for internal forms where polish doesn't matter.

For more on the redirect side specifically, see the custom redirects docs for the exact field names and modes SaveForm supports.

Form-submit success that just works

SaveForm supports default success pages, custom redirects, and JSON responses for fetch-driven flows. Pick the path that fits.

The 4 ways to handle form-submit success on a static site — SaveForm.io | SaveForm.io