Contact form for Next.js websites
Build a working contact form for your Next.js app in three steps. Use a client component or server action — both supported. No API route to maintain.
What your Next.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 Next.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 Next.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 Next.js code
Copy the Next.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 Next.js form will look like. Submitting opens a confirmation, no real request is sent.
Your Next.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
Set the access key as a public env var: `NEXT_PUBLIC_SPLITFORMS_KEY` — Next inlines it at build time, no exposure beyond what's already happening.
- 02
Lock the key to your domains in the Splitforms dashboard so a leaked key can't be replayed elsewhere.
- 03
Use the redirect field for users with JS disabled: `<input type="hidden" name="redirect" value="/thanks" />`. The fetch handler returns JSON for browsers; the redirect kicks in if the form posts traditionally.
- 04
Show submit state visually — disabled button + spinner — most users hammer submit on slow networks otherwise.
- 05
Validate email server-side too. Browser type="email" only checks format; spam bots send valid-format throwaway addresses.
What bites people who skip the docs.
Worth a 60-second skim before you ship to production. Each one has caused a Next.js support ticket at least once.
Don't expose your access key client-side without domain locking
Inlining access_key in a "use client" component makes the key visible to anyone who views source. That's fine if you've enabled allowed-domains in your Splitforms dashboard (Settings → Security) — anyone copying the key from your bundle can't use it from a different origin. If you haven't, use a server action so the key stays on the server.
Server actions need `'use server'` and a real form, not fetch
If you submit to a server action via <form action={myAction}>, Next handles the FormData serialization for you. If you call the action manually with fetch, you have to set the right Content-Type and stringify yourself. Pick one path and stick with it — mixing causes 'Server Action invalid' errors.
App Router + Suspense + useSearchParams = static-prerender bailout
If your form reads ?next=/something to support post-submit redirects, useSearchParams forces the page out of static generation. Either wrap the inner component in a <Suspense fallback={<Skeleton/>}> so the wrapper still prerenders, or accept dynamic rendering for the form route only.
Vercel Edge runtime can't read FormData from `multipart/form-data`
If your route handler uses export const runtime = 'edge' and the form posts as multipart (file inputs), it'll silently miss fields. Use application/x-www-form-urlencoded or remove the edge runtime declaration.
Don't put the form in a Server Component without `'use client'`
React 19 strict mode will warn about onSubmit handlers in server components. Either lift the form into a client component, or use a pure HTML form with a server action and no JS.
revalidatePath after submit nukes your optimistic UI
If your server action ends with revalidatePath('/contact') to refresh some cached data, Next refetches the route segment and replaces the rendered tree — your client-side status === 'ok' state survives the action call but the surrounding layout re-renders, often closing modals or re-mounting wrappers. Either skip revalidatePath when the contact page itself doesn't need it, or use revalidateTag scoped to the specific data you actually changed (a CRM cache, not the page route). useFormStatus + a tag-based revalidate is the clean combo.
How Next.js handles forms without splitforms.
The shape of the problem before splitforms enters the picture — and the gap it fills for Next.js specifically.
Without splitforms, you'd write a route handler at app/api/contact/route.ts, parse the FormData, configure SMTP via nodemailer or Resend (~10 minutes of secrets wrangling), add a Postgres or SQLite store for submissions, and then bolt on rate limiting, a honeypot check, an email-classifier or reCAPTCHA, and webhook fan-out. Server actions made the wiring slightly tidier in Next 14+, but the operational cost stays the same: a function with a runtime, a database, an outbound email provider, an inbox to monitor, and your name on the spam-filter incident report. Splitforms collapses all of that into a POST to a single URL.
Two ways to ship splitforms on Next.js.
Pick the pattern that matches your constraints — JS budget, key-exposure tolerance, server-side opacity. Both produce the same result.
Pattern A — server action (no client JS, key stays server-side)
Form posts to a server action; the action appends the access key from process.env.SPLITFORMS_KEY and proxies to splitforms. Works without JavaScript, key never reaches the bundle.
Pattern B — client component with fetch and inline status
'use client' component using useState for a 4-state status machine. Lets you show a spinner, inline errors, optimistic resets — at the cost of a hydration boundary. Use NEXT_PUBLIC_SPLITFORMS_KEY and rely on splitforms' domain-locking for safety.
Shipping Next.js + splitforms to production.
Host-specific gotchas, env-var conventions, and the boring-but-load-bearing details for putting this on the public internet.
On Vercel, the form works on every plan tier — server actions and client components both run inside the same edge/serverless function. Don't put the splitforms fetch inside a Vercel cron or background function (it's user-facing, latency matters). On Netlify with the Next runtime, server actions need the latest @netlify/plugin-nextjs (≥5.6). For self-hosted / Docker: configure output: 'standalone' in next.config.ts and pass SPLITFORMS_KEY as a runtime env var — never bake it into the image. For static export (output: 'export'), use the client-component path only — server actions aren't supported in static mode.
splitforms vs native next.js.
What you get for free vs what you build, pay for, or do without.
Server Action variant (App Router, no client JS)
If you want the form to work without JavaScript and keep the access key on the server, use a server action. Same outcome, no `"use client"` directive.
Things developers ask before they integrate.
Direct answers, no marketing fluff. Missing one? Email hello@splitforms.com.
Ship your Next.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.
