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.
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:
<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 covers them.
The fetch form submit
Now add the script. It takes over only when JavaScript runs:
<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
event.preventDefault()stops the browser’s normal submit, which would load a new page.new FormData(form)collects every field with aname, exactly as the browser would have sent it. You don’t need to read inputs one by one.Accept: application/jsontells 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.- Disabling the button while the request runs stops impatient double clicks from sending twice.
form.reset()clears the fields after success, so the visitor can see the message went.- The
catchblock 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
messagealongside thecode, 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>:
<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-stateon the form tosending,successorerror, so you can style each state in CSS. - Fires
formdoc:successandformdoc:errorevents you can listen for, for example to show your own message or record an analytics event.
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 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.
FormDataonly includes inputs with aname, and skips disabled inputs. - Response isn’t JSON. Without the
Accept: application/jsonheader, many endpoints reply with a redirect or HTML page, andresponse.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 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 is a good starting form for either, and you can get a free key at https://app.formdoc.dev/new.