Contact form for React websites
Plug-and-play contact form for any React app. Vite, Create React App, Remix, Gatsby — all work. One fetch call, full state handling.
What your React 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 React 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 React 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 React code
Copy the React 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 React form will look like. Submitting opens a confirmation, no real request is sent.
Your React 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 a single `status` state variable: `'idle' | 'loading' | 'ok' | 'err'`. Cleaner than 4 booleans.
- 02
Show inline success/error messages instead of alert() — much better UX, easier to test, accessible.
- 03
After successful submit, call `e.target.reset()` to clear the form. Users expect this; not doing it feels broken.
- 04
Add `aria-busy={status === 'loading'}` to the form element for screen readers.
- 05
If using a UI library (MUI, Chakra, Radix), use their Form/Input components for accessibility — splitforms doesn't care about your DOM as long as inputs have `name` attributes.
What bites people who skip the docs.
Worth a 60-second skim before you ship to production. Each one has caused a React support ticket at least once.
useState + setStatus inside async causes race conditions
If a user double-clicks submit before the first request settles, you'll fire two POSTs. Always disable the button while status === 'loading'. Better still, also use an AbortController to cancel the in-flight request if a re-submit happens.
FormData and controlled inputs can desync
If you control inputs via useState (value={name} onChange={...}), the FormData object you build with new FormData(e.target) won't see your state — it reads the actual DOM. Either use uncontrolled inputs (no value prop) or build the body manually from state.
CSP errors when posting to a third-party endpoint
If your site has a strict Content Security Policy with connect-src 'self', fetch to splitforms.com will be blocked. Add splitforms.com to your connect-src directive: connect-src 'self' https://splitforms.com.
Strict mode + double-mount triggers two submissions in dev
React 18+ strict mode mounts components twice in development to surface side effects. If you put your fetch call in useEffect (don't), you'll see double-submits. Always trigger network calls in event handlers, not effects.
Don't forget to e.preventDefault() on the form's onSubmit
Without preventDefault, the browser does its own form submission to wherever the form's action attribute points (or the current page) AND your handler runs — you get an unexpected page reload + your fetch call.
Stale closures in event handlers freeze your access key on hot reload
If you read process.env.REACT_APP_SPLITFORMS_KEY (CRA) or import.meta.env.VITE_SPLITFORMS_KEY (Vite) inside a useCallback or memoized handler, rotating the key in .env and saving doesn't update the captured value during HMR — only a full page refresh does. The form keeps POSTing the old key for the rest of the dev session and silently 401s. Read env vars at the module top level, or accept the key as a prop so React's normal reactivity propagates the new value.
How React handles forms without splitforms.
The shape of the problem before splitforms enters the picture — and the gap it fills for React specifically.
React itself ships nothing for form submission — it's a view layer. The historical baseline is one of: an Express/Hono/Fastify server you stand up just for POST /api/contact, a Function-as-a-Service (Vercel/Netlify/Cloudflare) that ends up needing the same SMTP wiring, or a third-party form library (React Hook Form, Formik) that handles validation but still leaves you to operate the backend. Vite, CRA, and Remix all default to assuming you have somewhere to POST — they just don't tell you where. Splitforms is the where: a single fetch call, no library install, no useEffect gymnastics, no Express boilerplate.
Two ways to ship splitforms on React.
Pick the pattern that matches your constraints — JS budget, key-exposure tolerance, server-side opacity. Both produce the same result.
Pattern A — uncontrolled inputs + native FormData
Skip useState per field. Inputs stay uncontrolled, new FormData(e.currentTarget) reads them at submit time, status state covers idle/loading/ok/err. ~25 lines, no form library.
Pattern B — React Hook Form for validation, splitforms for delivery
RHF handles client-side validation (zod schema, error messages); on valid submit, hand off to splitforms. RHF's handleSubmit callback receives parsed values — repackage as FormData and POST.
Shipping React + splitforms to production.
Host-specific gotchas, env-var conventions, and the boring-but-load-bearing details for putting this on the public internet.
Vite-built React apps are static — they deploy to any static host (Vercel, Netlify, Cloudflare Pages, S3, GitHub Pages). The splitforms fetch is cross-origin, so configure your CSP to allow connect-src 'self' https://splitforms.com if you have one. Vite reads env vars from .env at build time and only exposes those prefixed VITE_ to the browser bundle. CRA uses REACT_APP_ instead. On Cloudflare Pages, the build output is served from the edge with sub-50ms cold starts; splitforms adds another ~30ms RTT — not noticeable in practice. Lock the access key to your live origin in the splitforms dashboard.
splitforms vs native react.
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 React 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.
