splitforms.com
AJAX (VANILLA JS) · CONTACT FORM

Vanilla JavaScript AJAX contact form

No framework? No problem. Submit a form via the native `fetch()` API and show inline success/error messages — pure browser JavaScript, zero dependencies, no jQuery, no axios. Works in every modern browser back to Edge 18.

1,000 free submissions every month.·No credit card.
contact.htmlhtml37 lines
01<form id="contact" autocomplete="off">
02 <input type="text" name="name" placeholder="Name" required />
03 <input type="email" name="email" placeholder="Email" required />
04 <textarea name="message" placeholder="Message" required></textarea>
05 <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" />
06 <button type="submit">Send</button>
07 <p id="msg"></p>
08</form>
09
10<script>
11 const form = document.getElementById("contact");
12 const msg = document.getElementById("msg");
13
14 form.addEventListener("submit", async (e) => {
15 e.preventDefault();
16 msg.textContent = "Sending…";
17
18 const formData = new FormData(form);
19 formData.append("access_key", "YOUR_ACCESS_KEY");
20
21 try {
22 const res = await fetch("https://splitforms.com/api/submit", {
23 method: "POST",
24 body: formData,
25 });
26 const data = await res.json();
27 if (data.success) {
28 msg.textContent = "Thanks! We'll be in touch.";
29 form.reset();
30 } else {
31 msg.textContent = "Something went wrong: " + (data.message || "Try again");
32 }
33 } catch (err) {
34 msg.textContent = "Network error. Try again.";
35 }
36 });
37</script>
1,000
submissions / mo, free
14ms
median latency, edge
0
lines of backend code
17+
frameworks supported
✶ Live preview

What your AJAX (vanilla JS) contact form actually looks like.

Drop-in form backend with spam filtering, signed webhooks, and a real submissions dashboard. The same code in this preview is what you copy into your AJAX (vanilla JS) project — no SDK, no plugin, no PHP.

  • 1,000 submissions per month, free forever
  • Honeypot + AI spam classifier on every plan
  • Signed webhooks to Slack, Discord, your server
AJAX (vanilla JS) contact form on Splitforms — drop-in form backend with spam filtering and webhooks
§ 01Setup3 steps · 60 seconds · zero config

Ship a AJAX (vanilla JS) contact form without a backend.

No SDK, no PHP, no plugin. Your form posts standard FormData to one URL — submissions land in your inbox.

STEP 01GENERATE

Get your free access key

Verify your email and your access key is generated instantly. Free for 1,000 submissions per month, forever.

Create your form

By signing up, you agree to our terms and privacy policy.

STEP 02EMBED

Drop in the AJAX (vanilla JS) code

Copy the AJAX (vanilla JS) snippet on the right and paste it into your project. Replace YOUR_ACCESS_KEY with the key from step 1.

snippethtml
<form id="contact" autocomplete="off">
…
STEP 03RECEIVE

Submissions land in your inbox

Hits your dashboard and email in seconds. Forward to Slack, Discord, Sheets, Notion, or any signed webhook URL.

inbox · 1 newjust now
FROM contact@yoursite.com
New AJAX (vanilla JS) form submission
Maya Iyer maya@studio71.co
Loved the new pricing page — quick question about the 4-year plan. Are usage limits per project or account-wide?
§ 02Live demosandboxed · no key required · no submission sent

Try it now — no signup, no key.

This is a styled HTML preview of what your AJAX (vanilla JS) form will look like. Submitting opens a confirmation, no real request is sent.

preview · ajax (vanilla js)localhost:3000
✦ what just happened

Your AJAX (vanilla JS) form posts FormData to /api/submit. Splitforms validates the access key, runs the spam classifier, and forwards the parsed submission to your inbox plus the dashboard.

  • 14ms median round-trip from the edge.
  • Honeypot + classifier, no CAPTCHA.
  • Per-domain key locking out of the box.
REQUEST · POST /api/submit
{
  "access_key": "sk_live_4f9a_••••",
  "name":       "Maya Iyer",
  "email":      "maya@studio71.co",
  "message":    "…"
}
← 200 OK · { "success": true } · 14ms
§ 03Best practices5 rules · production-tested

How to ship this without regrets.

Five rules that make the difference between a form that works in the demo and a form that survives launch traffic.

  1. 01

    Use `form.elements` and FormData together, not querySelector chains. FormData(form) is one line and handles disabled/unchecked fields correctly.

  2. 02

    Set the submit button's text dynamically: 'Send' → 'Sending…' → 'Send' again. Users on slow networks need feedback or they re-click.

  3. 03

    Wrap the fetch in try/catch AND check `data.success`. Network errors and HTTP errors are different things — catch both.

  4. 04

    Use `aria-live="polite"` on the status `<p>` element so screen readers announce success/error messages without interrupting.

  5. 05

    After success, call `form.reset()` then re-enable the submit button. Easy to forget the re-enable when the form clears visually.

§ 04Common gotchas in AJAX (vanilla JS)6 edge cases worth knowing

What bites people who skip the docs.

Worth a 60-second skim before you ship to production. Each one has caused a AJAX (vanilla JS) support ticket at least once.

⚠ gotcha

Forgetting e.preventDefault() reloads the page

Without preventDefault, the browser does its own form submission to wherever the form's action attribute points (or the current page) AND your fetch runs. You see a flash, the page reloads, and your handler's effects are lost.

⚠ gotcha

FormData includes ALL form fields — even disabled ones get dropped

new FormData(form) skips inputs without a name attribute, skips disabled inputs, skips unchecked checkboxes/radios. If a field doesn't show up in your splitforms inbox, check whether it's disabled at submit time.

⚠ gotcha

fetch() doesn't reject on HTTP 4xx/5xx — only network errors

If splitforms returns a 401 (bad key) or 429 (rate limit), fetch resolves successfully. You have to check res.ok or data.success yourself. Wrapping in try/catch only catches network failures, not HTTP errors.

⚠ gotcha

Double-click submit fires two requests

Without disabling the button on the first click, a quick double-click sends two POSTs. Both succeed; the user sees one success message; you see two submissions. Always set button.disabled = true at the start of the handler.

⚠ gotcha

Strict CSP with connect-src 'self' blocks splitforms.com

If your site has a Content-Security-Policy header with connect-src 'self', fetch to splitforms.com is blocked. Add it explicitly: connect-src 'self' https://splitforms.com.

⚠ gotcha

Setting Content-Type manually breaks the multipart boundary

Common mistake: writing fetch(url, { method: 'POST', headers: { 'Content-Type': 'multipart/form-data' }, body: formData }). The browser silently fails to append the boundary parameter (; boundary=---WebKitFormBoundary…) because you've overridden its automatic header — splitforms's parser then sees a malformed body and returns 400. Fix: omit the headers object entirely. The browser sets Content-Type correctly when you pass a FormData instance as the body. Same trap when copying example code from old jQuery tutorials that hardcode the header.

§ 04bNative AJAX (vanilla JS) forms…and where they break down

How AJAX (vanilla JS) handles forms without splitforms.

The shape of the problem before splitforms enters the picture — and the gap it fills for AJAX (vanilla JS) specifically.

Vanilla JS / AJAX forms have been the no-framework default since jQuery's heyday. Without splitforms, the 'AJAX' part is one fetch line; the operational part is everything else: a backend route, an SMTP provider, a database for submissions, a honeypot or reCAPTCHA, a thank-you page, error handling for HTTP 4xx/5xx, retry logic. For 'JS-only on a static host' setups (Cloudflare Pages, GitHub Pages, S3), there's literally no server to run the route on — historically that meant Formspree, Formspark, Web3Forms, Basin. Splitforms is the modern entry: same shape, better free tier, better spam filtering, signed webhooks included.

§ 04cAlternative integration patterns2 ways to wire it

Two ways to ship splitforms on AJAX (vanilla JS).

Pick the pattern that matches your constraints — JS budget, key-exposure tolerance, server-side opacity. Both produce the same result.

PATTERN A

Pattern A — fetch + FormData + status element

Single submit listener. new FormData(form) reads inputs, append the access key, POST. Update an aria-live status <p> with the result. ~25 lines, no library.

pattern-a.htmlhtml16 lines
01<form id="cf">
02 <input name="email" type="email" required />
03 <textarea name="message" required></textarea>
04 <button type="submit">Send</button>
05 <p id="msg" aria-live="polite"></p>
06</form>
07<script>
08 document.getElementById("cf").addEventListener("submit", async (e) => {
09 e.preventDefault();
10 const fd = new FormData(e.currentTarget);
11 fd.append("access_key", "YOUR_ACCESS_KEY");
12 const r = await fetch("https://splitforms.com/api/submit", { method: "POST", body: fd });
13 const data = await r.json();
14 document.getElementById("msg").textContent = data.success ? "Thanks!" : (data.message || "Try again");
15 });
16</script>
PATTERN B

Pattern B — progressive enhancement (works without JS)

Form has a real action attribute and a redirect hidden field — works with JS disabled (browser POSTs natively, splitforms 302s). When JS is available, the listener intercepts for inline UX. Best of both worlds, no compromise.

pattern-b.htmlhtml13 lines
01<form id="cf" action="https://splitforms.com/api/submit" method="POST">
02 <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
03 <input type="hidden" name="redirect" value="/thanks.html" />
04 <input name="email" type="email" required />
05 <button type="submit">Send</button>
06</form>
07<script>
08 document.getElementById("cf").addEventListener("submit", async (e) => {
09 e.preventDefault();
10 const r = await fetch(e.target.action, { method: "POST", body: new FormData(e.target) });
11 if ((await r.json()).success) location.href = "/thanks.html";
12 });
13</script>
§ 04dDeployment notes for AJAX (vanilla JS)hosting · env vars · CSP

Shipping AJAX (vanilla JS) + splitforms to production.

Host-specific gotchas, env-var conventions, and the boring-but-load-bearing details for putting this on the public internet.

Vanilla JS deploys to any static host — the snippet is HTML + inline <script>, no build step. CSP: if your site sets connect-src 'self', add https://splitforms.com to the directive or fetch is blocked. Browser support: native fetch is in every browser back to Edge 18 — the snippet runs without polyfills on every market-share-relevant browser. The progressive-enhancement variant (Pattern B) keeps the form working when JS fails to load — useful on flaky networks, ad-blocked clients, or for accessibility tools that disable JS.

§ 05Comparisonvs native ajax (vanilla js)

splitforms vs native ajax (vanilla js).

What you get for free vs what you build, pay for, or do without.

FeatureNative AJAX (vanilla JS)splitforms
Dependencies0 (vanilla JS) or jQuery (28kb)0 — uses native fetch
Setup timeBackend + email + spam (1 day+)Paste 25 lines
Spam filterDIY honeypot + reCAPTCHABuilt-in
Submission storageServer + DBDashboard included
Browser supportDepends on your codeEdge 18+, all modern browsers
CostHosting + email providerFree (1,000/mo)
§ 07Questions6 answered

Things developers ask before they integrate.

Direct answers, no marketing fluff. Missing one? Email hello@splitforms.com.

01How do I add an AJAX contact form without a framework?
Copy the HTML + script snippet above into any page. Replace YOUR_ACCESS_KEY with your splitforms key. The script attaches a submit listener, builds FormData, posts to splitforms.com, and renders the response inline. No build step, no npm install.
02Does splitforms work with jQuery's $.ajax / $.post?
Yes. Use jQuery's $.post or $.ajax with processData: false, contentType: false so it doesn't double-encode the FormData. Modern browsers don't need jQuery for this — but the splitforms endpoint accepts requests from any HTTP client.
03How do I handle form errors with vanilla JavaScript?
Check data.success after parsing the JSON response. If false, render data.message into your status element. Wrap the fetch in try/catch for network errors, and check res.ok for HTTP-level errors — they're three separate failure modes.
04Can I use splitforms with axios, ky, or other fetch libraries?
Yes — anything that POSTs FormData to a URL works. The splitforms endpoint doesn't care about the client library, only about the request body and the access_key field.
05How do I customize the success / redirect behavior?
Two options. (1) Inline success: render a styled <p> with aria-live after a 2xx response (default in our snippet). (2) Redirect: instead of using fetch, set form.action = 'https://splitforms.com/api/submit' and <input name="redirect"> — let the browser submit natively for a server-side 302.
06Does this work without JavaScript at all?
The AJAX version requires JS. For a no-JS fallback, also add action="https://splitforms.com/api/submit" method="POST" to the form tag and a hidden redirect field. With JS, your handler intercepts; without JS, the browser does a native form POST and follows the redirect.
✻ ✻ ✻

Ship your AJAX (vanilla JS) contact form in 60 seconds.

1,000 free submissions per month. No credit card. Lock the access key to your domains, paste the snippet, watch submissions land in your inbox.

Get free access key →Read the docs
v0.1 · founders pricing locked in · early access open