Skip to content
FFormhook
Back to blog

Blog

AJAX form submission: send a form without a page reload

· 4 min read

Submit an HTML form the plain way and the browser does what it has always done: navigate to a new page, or reload the current one, throwing away whatever was on screen a moment earlier. For a form that lives on its own page, that's harmless. For a form inside a modal, a chat widget, a checkout flow, or any single-page app, a full navigation is a jarring reset - the modal closes, the app remounts, the scroll position is gone. AJAX submission - sending the form data with JavaScript's fetch() instead of letting the browser navigate - fixes that, and it's a handful of lines.

Why skip the page reload

A full-page reload discards everything JavaScript was holding in memory: an open modal, a wizard's current step, unsaved state elsewhere on the page. If the form lives inside a React, Vue, or Svelte component tree, letting the browser navigate away also tears down the whole client-side app and reloads it from scratch.

Submitting through fetch() keeps the page - and whatever app is running on it - intact. You decide what happens next: show a success message inline, close the modal, reset the fields, redirect programmatically if you want to. It's the difference between the browser deciding and you deciding.

It's an enhancement, not a requirement

You don't have to choose one approach over the other. Leave the action and method attributes on the <form> tag exactly as they'd be for a plain POST, and only intercept the submit event with JavaScript when it's available. If a script fails to load, an extension interferes, or a visitor has JavaScript disabled, the form still works - it just falls back to a normal page navigation instead of an inline status message. That's the whole idea of progressive enhancement: the base case, an HTML form posting straight to https://formhook.app/f/{apiKey}, always works on its own. JavaScript only makes it nicer when it's there.

A minimal example

No framework needed - this is the whole pattern in vanilla JavaScript. It prevents the default navigation, sends the form data as FormData, and swaps in a status message depending on the result:

const form = document.querySelector("#contact-form");
form.addEventListener("submit", async (e) => {
  e.preventDefault();
  const res = await fetch(form.action, {
    method: "POST",
    body: new FormData(form),
    headers: { Accept: "application/json" },
  });
  if (res.ok) {
    form.reset();
    document.querySelector("#form-status").textContent = "Thanks - we got it.";
  } else {
    document.querySelector("#form-status").textContent = "Something went wrong. Please try again.";
  }
});

Ask for JSON, not a redirect

Notice the Accept: application/json header in the example above. Without it, a form backend built to also serve plain HTML submissions might try to redirect the browser to a "thank you" page - correct behavior for a normal POST, but useless when you're calling it from fetch(). Sending Accept: application/json tells Formhook's endpoint that the request came from JavaScript, so it returns a JSON success or error response instead of a redirect, and your code can branch on res.ok the way the example does. See the docs for the exact request format.

Don't forget accessibility

Swapping in a status message with textContent is invisible to a screen reader unless the browser is told to announce it. Add aria-live="polite" to the status element (the one document.querySelector("#form-status") is targeting in the example) and assistive technology will read the update out loud the moment it changes - success or error - without the visitor needing to find and re-focus it themselves. It's one attribute, and it's the difference between a form that works for everyone and one that quietly excludes screen reader users the moment JavaScript takes over.

None of this requires a library or an SDK - a Formhook endpoint accepts a plain form POST from any host, and the fetch() version above is optional progressive enhancement on top of that, not a prerequisite. See the docs for the full request/response reference, and pricing for the free tier's limits.

Ship a working form in one line

EU-hosted, submissions kept forever, push notifications on every tier.

Start free

No credit card · read the docs

Keep reading