Forms

Prevent Contact Form Reload With the Fetch API

A plain HTML form reloads the page on submit because the browser treats it as a top-level navigation. Intercept the submit event with fetch and you keep the page, the state, and the user.

· The SimpleForm Team

Your contact form works. Someone clicks submit, the browser flashes white, and the page reloads with a stray query string hanging off the URL. If you built a custom "message sent" note with JavaScript, it disappears the instant the reload happens. If you are inside a single-page app, the whole client-side state resets to its first-load values. That is the default behavior of every plain HTML form, and it costs you the polished submit experience a modern static site is supposed to have.

SimpleForm is a hosted form backend for static websites: you point a form's action attribute at a SimpleForm endpoint, and the endpoint can hand back a plain JSON response instead of forcing a browser-native page navigation. Understanding why the reload happens in the first place is the first step to removing it.

Why Does a Form Reload the Page When You Click Submit?

A native HTML form submit is not an API call. It is a top-level browser navigation, identical in kind to typing a URL into the address bar and pressing enter. When the browser sees a form's submit event fire without interference, it packages the field values per the method attribute (GET appends them to the URL, POST puts them in the request body) and navigates the current tab to the action URL, discarding the current document and everything running in it.

This behavior predates JavaScript itself and the browser will keep doing it forever for backward compatibility, per the WHATWG HTML Standard's form submission algorithm. Nothing about your CSS, your framework, or your hosting provider changes it. The only way to stop it is to intercept the submit event in your own script before the browser gets to it.

What Happens to Your Page State When the Browser Navigates?

A full-page navigation tears down the current document and loads a new one, even if the new document looks identical. Any JavaScript variables, timers, open modals, scroll position, or client-side router state are gone. A confirmation banner you render with JavaScript never survives that trip, because the script that would render it is unloaded along with the rest of the page.

This matters most on static sites built with a client-side router, where the whole point of the architecture is to avoid full navigations after the first load. A contact form that reloads the page undoes that benefit for the one interaction most likely to convert a visitor into a lead.

Submitting a form with the browser's default behavior is the quickest way to ship a working contact form on a static site, and for a large share of forms it is genuinely good enough. Where it stops being good enough is the moment you want a success state that persists, an inline error message, or a submit button that shows a loading spinner while the request is in flight. At that point you need the endpoint itself to speak JSON, so your script can read and react to the response. SimpleForm's endpoints accept a standard HTML form post and can return either a redirect or JSON, so the same endpoint works whether or not you intercept the submit — see the docs for the exact request and response shape.

How Do You Submit a Form Without Reloading the Page?

The mechanism has three parts: stop the browser's default action, send the data yourself, and update the page based on what comes back.

  1. Attach a submit event listener to the form element instead of relying on the action attribute alone.
  2. Call event.preventDefault() first, before anything else, so the browser never starts its own navigation.
  3. Build a FormData object from the form element, which automatically captures every named field, including file inputs.
  4. Pass the FormData to fetch(), targeting the same endpoint URL the action attribute already points to, with the method set to POST.
  5. Await the response, check whether the status code indicates success, and read the JSON body if the endpoint returns one.
  6. Update the DOM yourself: show a success message, clear the fields, or display the specific error the endpoint returned.

None of this requires a framework. A form, a script tag, and about fifteen lines of vanilla JavaScript are enough. The endpoint side needs to cooperate too: it has to accept the same POST body a plain form would have sent, and it has to respond with something other than an HTTP redirect when it detects the request came from a script rather than a full-page navigation.

What Is the Difference Between a Default Submit and a Fetch Submit?

BehaviorDefault HTML submitFetch-based submit
Page navigationFull reload, current document discardedNone — the page stays put
Success feedbackWhatever the destination page showsYour script controls the message and timing
Client-side stateReset to initial loadPreserved
Works with JavaScript disabledYesNo — needs a fallback action attribute
Response format neededHTML page or redirectJSON

How Do You Handle Success and Error Responses?

Once the fetch call resolves, branch on the HTTP status. A 200 or 201 means the submission was accepted; show the confirmation copy and clear the form fields so a second accidental click does not duplicate the entry. A 4xx status means the request was rejected — a missing required field, a spam filter catch, or a rate limit — and the response body should tell you which. A 402 status specifically signals a plan limit was exceeded; SimpleForm returns that code when a form goes over its monthly submission cap, and the submission is not stored or emailed when it does. Treat a network failure (fetch throwing before you get a response at all) as its own case with a generic retry message, since you cannot know if the server received the data.

What About Users Without JavaScript Enabled?

Progressive enhancement means the form works both ways. Leave the action and method attributes on the form element exactly as they would be for a plain submit. Your event listener calls preventDefault() only when it successfully attaches and runs, so a user with JavaScript disabled, or a request from a bot that does not execute scripts, falls through to the ordinary full-page submission and lands on whatever redirect the endpoint is configured to send. A custom redirect URL is available on every SimpleForm plan, including Free, specifically to support this fallback path without any extra configuration.

The two paths share one endpoint and one set of validation rules, so you are not maintaining two versions of your spam protection or your required-field logic — only two ways of displaying the outcome.

Is It Worth Rewriting a Form That Already Works?

If your form's default reload behavior is not actually costing you conversions, leave it alone; a working contact form beats a broken one regardless of how it submits. The case for switching is narrower and more concrete: a single-page app where a reload breaks navigation state, a form embedded in a modal that should not close the whole page, or a submit button where users need visible feedback that something is happening before the request finishes. In each of those cases the fix is additive — you keep the same action URL and the same endpoint, and you add roughly fifteen lines of script on top. There is no backend to stand up and no migration of existing submissions, because the endpoint itself does not change.

Send a Submission Without Reloading the Page

Create a free SimpleForm endpoint, wire the fetch snippet above to it, and open your browser's network tab to watch the JSON response come back with no page reload in sight — the whole check takes about a minute and does not need a credit card. Start for free and confirm it against your own form.

Frequently asked questions

The listener is either not attached before the first click, or it is missing event.preventDefault(). Without that call, the browser proceeds with its default top-level navigation regardless of any other code in the handler. Confirm the listener is bound after the DOM is ready and that preventDefault() runs as the very first line.

Yes. Build a FormData object from the form element and pass it directly to fetch as the request body — do not set a Content-Type header yourself, since the browser sets the correct multipart boundary automatically. File inputs are included in FormData the same way text inputs are.

A 200 or 201 status is standard for an accepted submission. Pair it with a small JSON body so the client script has something explicit to check, rather than inferring success purely from the absence of an error status.

Only if you remove the action and method attributes from the form element. Leave them in place and only call preventDefault() inside a working event listener; a user without JavaScript then falls through to the ordinary full-page submission and redirect, which keeps the form functional either way.

It signals the account has exceeded its monthly submission limit. The specific submission is rejected, not stored, and not emailed, but the form itself keeps accepting other submissions normally, and the limit resets at the start of the next month.

Ship a working form in five minutes. Point your form's action at a SimpleForm endpoint and submissions land in your inbox and dashboard straight away. Start free or read the docs.

More from the blog