Why contact forms ship broken
Contact forms are the most under-tested critical path on the web. They're built last, demoed once on localhost, and then trusted to deliver leads for years. The failure modes are uniquely sneaky because a broken form looks identical to an unpopular one — no errors, no alerts, just silence you misread as "slow month."
Worse, the local demo proves almost nothing. Localhost has no CDN caching, no CORS boundary, no CSP headers, no real DNS, and no spam pressure. The checklist below is ordered so the highest-yield tests come first; every check names the tool or command to run it.
Pass 1: the end-to-end test (do this even if you do nothing else)
On the deployed production site — not localhost, not the preview URL with auth in front of it:
- Submit a realistic entry: real-shaped name, an email you control, a multi-line message.
- Visitor side: a clear success state appears (message or thank-you page). The form either clears or disables — it shouldn't invite a confused double-click.
- Owner side: the notification lands in the inbox — search spam too — within ~60 seconds, the visitor's details are complete, and hitting Reply addresses the visitor (Reply-To set correctly), not a noreply address.
- Durability: the submission exists somewhere besides email — a dashboard or database — so a future mail failure can't lose a lead.
- Refresh the success page: no "Confirm Form Resubmission" popup, no duplicate entry.
While you're there, open DevTools → Network and confirm the POST returns 200 with no redirect chain in front of it. Anything else, detour to the debugging guide (or the 405 guide if you're on a static host) before continuing.
Pass 2: validation — hostile and legitimate inputs
Validation testing has two halves, and teams reliably skip the second.
Inputs that must be rejected (politely)
- Empty required fields and whitespace-only values (
" "passes a naiverequiredcheck on the server if you don't trim). - Malformed emails:
a@b,user@.com,user@domain. - A 50,000-character paste into the message field — you want a length cap with a readable error, not a 500 or a truncated lead.
- The error message must say which field and why, keep the visitor's other input intact, and move focus to the offending field.
Inputs that must be accepted (this is where real users get rejected)
- Plus-addressed emails:
user+tag@gmail.comis valid; regexes that reject+reject real customers. - Names with apostrophes, hyphens, and diacritics: O'Brien, Anne-Marie, José, Nguyễn — and non-Latin scripts entirely (田中).
- Emoji and URLs in the message body. Then check where submissions are displayed: a message containing
<script>alert(1)</script>must render as text, never execute. If it executes anywhere (email template, dashboard, Slack), you have a stored-XSS bug, not a form bug.
Also disable JavaScript once (DevTools → Cmd/Ctrl+Shift+P → "Disable JavaScript") and submit. If your form only validates client-side, the server must still enforce the same rules — client validation is UX, server validation is the contract.
Pass 3: spam defenses — attack your own form
Bots don't use your UI; they POST straight to the endpoint. So should your test:
# Simulate a dumb bot: fills every field, including the honeypot
curl -i -X POST https://splitforms.com/api/submit \
-d "access_key=YOUR_ACCESS_KEY" \
-d "email=bot@example.com" \
-d "message=cheap pills" \
-d "botcheck=on" # the hidden honeypot a human never seesExpected result: the submission is rejected or silently discarded, and no notification email is sent. Then verify the inverse — the human path still works with browser autofill, a password manager, and an auto-translate extension active, because those tools occasionally trip naive bot heuristics.
Three checks on the defense itself:
- The honeypot field is invisible to humans and excluded from autofill (
autocomplete="off",tabindex="-1") — otherwise a password manager fills it and your real visitors get classified as bots. - If you use a CAPTCHA, test it failing: block the script domain and confirm the form degrades with a readable error instead of a dead button.
- Rate limiting: ten rapid scripted submissions shouldn't produce ten emails.
Trade-offs between approaches are covered in honeypot vs reCAPTCHA and the complete spam protection guide; you can fire test submissions at a live demo endpoint on the spam test page.
Pass 4: email deliverability — score it, don't guess
One email arriving in your inbox today doesn't prove deliverability — your own domain is the friendliest possible recipient. Score it:
- Go to
mail-tester.com, copy the disposable address it generates. - Set that address as your form's notification recipient (temporarily) and submit.
- Read the score. 9/10 or better is the bar. Below that, the report names the failure — usually SPF, DKIM, or DMARC alignment.
Then send one test each to a Gmail, an Outlook/Microsoft 365, and (if your customers skew Apple) an iCloud address, and check the actual headers: in Gmail, three-dot menu → Show original → SPF/DKIM/DMARC should all read PASS. Failures here are fixed in the spam-folder guide; emails that vanish entirely are the no-email guide. For ongoing test traffic that never pollutes a real inbox, see testing form submissions without real emails.
Pass 5: mobile — emulator first, then a real phone
DevTools' device toolbar catches layout; real devices catch behavior. On an actual iPhone (Safari) and Android phone (Chrome):
- Keyboards:
type="email"shows the @ keyboard,type="tel"the dial pad,inputmode="numeric"where appropriate. Wrong keyboards triple typo rates. - No zoom-jump: inputs styled below 16px font-size make iOS Safari zoom on focus and disorient the page. Set 16px+.
- The keyboard doesn't hide the action: with the keyboard open, the visitor can still scroll to the submit button; nothing sticky covers it.
- Autofill: proper
autocompletetokens (name,email,tel) so one tap fills the form — the single biggest mobile conversion lever. - Slow network: throttle to 3G; the button shows a pending state and can't be double-submitted, producing duplicate emails.
Pass 6: accessibility — keyboard, labels, announced errors
- Keyboard-only pass: unplug the mouse. Tab order matches visual order, focus is visible on every control, Enter submits, and the success state receives focus or is announced.
- Labels: every input has a real
<label for>. Placeholders are not labels — they disappear on input, have failing contrast, and screen readers treat them inconsistently. - Errors: tie messages to fields with
aria-describedby, mark the fieldaria-invalid="true", and announce the summary in anaria-live="polite"region. Color alone (a red border) fails WCAG 1.4.1. - Automated sweep: Lighthouse accessibility audit or axe DevTools. Treat them as the floor — they catch roughly a third of real issues — then do one VoiceOver (Cmd+F5) or NVDA pass yourself.
Keep it passing after launch
Forms rot. Quotas exhaust mid-month, a redesign covers the button, a CSP header added for another feature blocks the fetch, an email provider tightens filtering. Re-run pass 1 on every deploy that touches the page, and put a monthly reminder on the calendar for the rest.
The structural fix is making form health observable. This is most of why we built splitforms: every submission is stored in a dashboard independent of email delivery, spam rejections are logged where you can audit them, and a quiet week is distinguishable from a broken form at a glance. The integration is one HTML attribute — action="https://splitforms.com/api/submit" — so there's no server code to re-test. Free for 500 submissions/month; start from the free contact form template, poke a live form on the test form page, or read the docs.
FAQ
What is the minimum test before a contact form goes live?
One full end-to-end pass on the deployed site (never just localhost): submit a realistic entry, confirm the visitor sees a clear success state, confirm the notification email arrives in the owner's inbox — not spam — within a minute, and confirm the submission is stored somewhere durable (dashboard or database) in case the email leg ever fails. If you only have five minutes, this one pass catches the failures that actually lose leads. Everything else in the checklist hardens against rarer cases.
How do I test a contact form without sending real emails to my inbox?
Three options, in increasing rigor: use a plus-alias (you+test@gmail.com) with a filter so test traffic skips your inbox but remains verifiable; use mail-tester.com, which gives you a disposable address plus a deliverability score out of 10; or use a capture service like Mailtrap or your form backend's dashboard, which records the submission and the email's delivery status without a real mailbox involved. Never test against a fake domain like test@test.com — the bounces hurt your sender reputation.
What inputs should I use to test form validation?
Test both directions. Should be rejected: empty required fields, whitespace-only values, malformed emails (a@b, a@b., user@.com), and over-length input (paste 50,000 characters into the message). Should be accepted: plus-addressed emails (user+tag@gmail.com), apostrophes and hyphens in names (O'Brien, Anne-Marie), accented and non-Latin characters (José, 田中), emoji in the message, and URLs or HTML tags in the message body — which must also render escaped, not executed, wherever you display them. Rejecting legitimate names is a more common production bug than accepting bad emails.
How do I test my form's spam protection without waiting for real bots?
Simulate a bot: submit directly to the endpoint with curl, filling the honeypot field a human can't see — that should be rejected or silently dropped. Submit faster than a human could type (a script that posts within a second of page load) if your form has a time-based trap. Then verify the inverse: a real human submission with autofill, password managers, and translation extensions enabled still goes through. Spam defense that blocks customers is worse than spam itself; see our honeypot vs reCAPTCHA comparison for the trade-offs.
How do I test a contact form on mobile properly?
Emulators catch layout issues; only real devices catch the rest. On an actual phone, check: the email field shows the email keyboard (type="email") and the phone field the number pad (type="tel"); inputs are at least 16px font-size so iOS Safari doesn't zoom-jump on focus; the submit button isn't covered by the keyboard or a sticky banner; autofill works; and the success state is visible without scrolling. Test on both iOS Safari and Android Chrome — their keyboard, autofill, and viewport behaviors differ meaningfully.
What accessibility checks does a contact form need?
Keyboard: Tab reaches every field in a logical order, Enter submits, and focus is always visible. Labels: every input has a programmatically associated <label> (placeholder text is not a label — it vanishes on input and fails WCAG). Errors: validation messages are announced to screen readers (aria-describedby on the field plus aria-live on the message region), and errors are identified by text, not color alone. Run axe DevTools or Lighthouse's accessibility audit for the automated 30%, then do one full keyboard-only pass and one VoiceOver/NVDA pass yourself.
How often should I re-test a working contact form?
On every deploy that touches the page (a CSS change can cover the button; a CSP header change can kill the fetch), after any DNS or email-provider change, and on a calendar — monthly is reasonable for a business where leads matter. Forms fail silently in production: quotas exhaust, certificates rotate, third-party scripts break. A monthly scheduled reminder plus a form backend dashboard you can glance at (submissions still flowing?) catches the drift. Some teams point an uptime monitor at the endpoint with a synthetic POST flagged as a test.
Want the structural fix so form health is always visible? Try splitforms free — every submission is stored in a dashboard, so a broken email never means a lost lead.
Related: debug a form that fails these tests, HTML-level submission bugs, and the refresh-resubmission popup.