# How to submit a form without page reload

> Submit a form without page reload using fetch, with a working AJAX form example, error handling, and a no-code option that keeps plain HTML working.

Developers

Submit a form without page reload using fetch, with a working AJAX form example, error handling, and a no-code option that keeps plain HTML working.

By [Venkatesh](https://www.linkedin.com/in/ageven/) 7 November 2026 · 6 min read

![Cover image: How to submit a form without page reload, a FormDoc guide](https://www.formdoc.dev/images/blog/submit-form-without-page-reload.png)

To submit a form without page reload, listen for the form’s `submit` event, call `event.preventDefault()`, and send the data yourself with `fetch` and `FormData`. Then show a success or error message inside the page. This is what people mean by an AJAX form. Below is a complete fetch form submit example, the details that make it robust, and a one-line option if you’d rather not write the JavaScript.

## Start with a form that works without JavaScript

Build the plain HTML version first. If the script fails to load, or a visitor has JavaScript turned off, the form should still send:

```html
<form id="contact" action="https://api.formdoc.dev/v1/submit" method="POST">
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY">

  <label for="email">Email</label>
  <input id="email" type="email" name="email" required>

  <label for="message">Message</label>
  <textarea id="message" name="message" required></textarea>

  <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" autocomplete="off">
  <button type="submit">Send</button>
  <p id="status" role="status" aria-live="polite"></p>
</form>
```

The empty `status` paragraph is where messages will appear. `role="status"` and `aria-live="polite"` make screen readers announce the result. If the `action`, `method` and `name` attributes are new to you, [HTML form action and method, explained](https://www.formdoc.dev/blog/html-form-attributes-explained/) covers them.

## The fetch form submit

Now add the script. It takes over only when JavaScript runs:

```html
<script>
  const form = document.getElementById("contact");
  const status = document.getElementById("status");
  const button = form.querySelector("button[type=submit]");

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    button.disabled = true;
    status.textContent = "Sending…";

    try {
      const response = await fetch(form.action, {
        method: "POST",
        body: new FormData(form),
        headers: { Accept: "application/json" },
      });
      const result = await response.json();

      if (result.success) {
        form.reset();
        status.textContent = "Thanks! Your message has been sent.";
      } else {
        status.textContent = result.error.message;
      }
    } catch (error) {
      status.textContent = "Couldn’t send. Please check your connection and try again.";
    } finally {
      button.disabled = false;
    }
  });
</script>
```

## What each step does

1.  **`event.preventDefault()`** stops the browser’s normal submit, which would load a new page.
2.  **`new FormData(form)`** collects every field with a `name`, exactly as the browser would have sent it. You don’t need to read inputs one by one.
3.  **`Accept: application/json`** tells the server you want a JSON reply instead of a redirect. FormDoc then answers with `{ "success": true, "submission_id": "…" }`, or `{ "success": false, "error": { "code", "message" } }` when something is wrong.
4.  **Disabling the button** while the request runs stops impatient double clicks from sending twice.
5.  **`form.reset()`** clears the fields after success, so the visitor can see the message went.
6.  **The `catch` block** handles network failures, such as a dropped mobile connection, separately from errors the server reports.

Don’t set a `Content-Type` header yourself when sending `FormData`. The browser sets it, including the boundary it needs.

## Make the result easy to notice

When the page doesn’t change, visitors can miss what happened. A few small touches make the result obvious to everyone:

-   **Put the message close to the button.** That’s where the visitor is looking when they press it.
-   **Use plain words.** “Thanks! Your message has been sent.” is clearer than “Success”. For errors, say what to do next.
-   **Show the server’s message for errors.** FormDoc’s error responses include a readable `message` alongside the `code`, so you can show it directly or map common codes to your own wording.
-   **Don’t rely on colour alone.** A green border is a nice extra, but the text should carry the meaning.
-   **Keep the visitor’s input on error.** Only call `form.reset()` after success, so nobody has to retype a long message.

If you replace the whole form with a thank-you note on success, move keyboard focus to that note (give it `tabindex="-1"` and call `.focus()`), so keyboard and screen reader users land in the right place.

## FormData vs JSON body

You can also send JSON. Both work with FormDoc; pick whichever fits your code.

|  | `FormData` body | JSON body |
| --- | --- | --- |
| How to build it | `new FormData(form)` | `JSON.stringify(Object.fromEntries(new FormData(form)))` |
| Header to set | None (browser sets it) | `Content-Type: application/json` |
| Matches the no-JavaScript submit | Yes | No |
| Checkbox groups with one name | Kept as several values | Only the last value unless you handle it |
| Best for | Plain HTML forms | Apps that already hold data as objects |

For a normal website form, `FormData` is simpler and behaves exactly like the browser’s own submit.

## The no-code option: formdoc.js

If you’re using FormDoc, you can skip the script above. Add this once, before `</body>`:

```html
<script src="https://api.formdoc.dev/formdoc.js" defer></script>
```

With it, your FormDoc form behaves like the AJAX form above:

-   Sends the form with fetch and shows success or an error inside the form, with no page change.
-   Disables the submit button while sending.
-   Sets `data-formdoc-state` on the form to `sending`, `success` or `error`, so you can style each state in CSS.
-   Fires `formdoc:success` and `formdoc:error` events you can listen for, for example to show your own message or record an analytics event.

```css
form[data-formdoc-state="sending"] button { opacity: 0.6; }
form[data-formdoc-state="success"] { border-color: green; }
```

Without JavaScript, the form still posts normally and visitors see the thank-you page. The [JavaScript docs](https://www.formdoc.dev/docs/javascript/) list every option.

## Things that go wrong

A few problems come up again and again with forms submitted by fetch:

-   **The page still reloads.** The listener isn’t attached, usually because the script runs before the form exists. Put the script after the form or use `defer`.
-   **Fields are missing.** `FormData` only includes inputs with a `name`, and skips disabled inputs.
-   **Response isn’t JSON.** Without the `Accept: application/json` header, many endpoints reply with a redirect or HTML page, and `response.json()` throws.
-   **Nothing tells the visitor what happened.** Always show a message for both outcomes, and make it reachable to screen readers.
-   **A framework re-renders the form.** In React, Vue or Svelte, handle submit in the component instead. The [React guide](https://www.formdoc.dev/docs/react/) shows that pattern.

## Common questions

### Is an AJAX form better than a normal form submit?

It feels smoother, because the visitor stays on the page and sees the result straight away. A normal submit is simpler and needs no JavaScript. The best approach is both: a form that works without JavaScript, enhanced with fetch when it’s available.

### Do I need jQuery to submit a form without reloading?

No. `fetch` and `FormData` are built into every modern browser and do everything older jQuery examples did, with less code.

### Will spam protection still work with fetch?

Yes. The honeypot field is part of the form, so `FormData` sends it like any other field, and spam scoring and rate limits run on the server either way. Bots caught by the honeypot get a normal success response.

## Try it

Pick the approach that suits you: the fetch script above for full control, or `formdoc.js` for no code at all. The [contact form template](https://www.formdoc.dev/templates/contact/) is a good starting form for either, and you can get a free key at [https://app.formdoc.dev/new](https://app.formdoc.dev/new).

Written by**[Venkatesh](https://www.linkedin.com/in/ageven/)**
