AJAX contact form (no page reload)
The default form POST navigates away, which is fine until the form lives in a modal, a sidebar, or halfway down a landing page you do not want to lose. This version submits with fetch and swaps the form for a success message in place. The unglamorous parts - disabling the button during submit, showing a real error on failure - are included, because they are the difference between a demo and something you ship.
Copy-paste HTML
<form id="contact-form" action="https://send.webforms.to/wft_your_key" method="POST">
<label for="name">Name</label>
<input id="name" name="name" type="text" required>
<label for="email">Email</label>
<input id="email" name="email" type="email" required>
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<button type="submit">Send</button>
<p id="form-status" role="status" aria-live="polite"></p>
</form>
<script>
const form = document.getElementById("contact-form");
const status = document.getElementById("form-status");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const button = form.querySelector("button");
button.disabled = true;
status.textContent = "Sending...";
try {
const res = await fetch(form.action, {
method: "POST",
body: new FormData(form),
headers: { Accept: "application/json" },
});
if (res.ok) {
form.reset();
status.textContent = "Thanks - your message is on its way.";
} else {
status.textContent = "Something went wrong. Please try again.";
}
} catch {
status.textContent = "Network error. Check your connection and retry.";
} finally {
button.disabled = false;
}
});
</script>Replace https://send.webforms.to/wft_your_key with your own endpoint from the WebForms dashboard.
Why these fields
- The Accept: application/json header tells the endpoint to answer with JSON instead of redirecting - without it, fetch follows the redirect and you cannot tell success from failure cleanly.
- The button is disabled during submit to prevent double-sends, and re-enabled in finally so a network error does not leave the form permanently stuck.
- The status element has role=status and aria-live=polite, so screen readers announce the outcome without focus tricks.
- Native browser validation still runs before the submit handler - required and type=email do their jobs with zero JS.
Where to send it
Get your endpoint
Free for 300 submissions a month. No card required.
Start freeAJAX contact form FAQ
For one form, sixty lines of vanilla JS beats a dependency. Libraries earn their keep on multi-step or heavily validated forms, not a contact form.
fetch, FormData and async/await are supported in every browser released since roughly 2017. If you support older, the classic POST fallback still works because the form has a real action attribute.