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.
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
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.
Get your free access key
Verify your email and your access key is generated instantly. Free for 1,000 submissions per month, forever.
By signing up, you agree to our terms and privacy policy.
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.
Submissions land in your inbox
Hits your dashboard and email in seconds. Forward to Slack, Discord, Sheets, Notion, or any signed webhook URL.
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.
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.
{
"access_key": "sk_live_4f9a_••••",
"name": "Maya Iyer",
"email": "maya@studio71.co",
"message": "…"
}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.
- 01
Use `form.elements` and FormData together, not querySelector chains. FormData(form) is one line and handles disabled/unchecked fields correctly.
- 02
Set the submit button's text dynamically: 'Send' → 'Sending…' → 'Send' again. Users on slow networks need feedback or they re-click.
- 03
Wrap the fetch in try/catch AND check `data.success`. Network errors and HTTP errors are different things — catch both.
- 04
Use `aria-live="polite"` on the status `<p>` element so screen readers announce success/error messages without interrupting.
- 05
After success, call `form.reset()` then re-enable the submit button. Easy to forget the re-enable when the form clears visually.
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.
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.
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.
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.
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.
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.
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.
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.
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 — 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 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.
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.
splitforms vs native ajax (vanilla js).
What you get for free vs what you build, pay for, or do without.
Things developers ask before they integrate.
Direct answers, no marketing fluff. Missing one? Email hello@splitforms.com.
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.
