splitforms.com
NEXT.JS · CONTACT FORM

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.

1,000 free submissions every month.·No credit card.
contact.tsxtsx35 lines
01"use client";
02import { useState } from "react";
03
04export default function ContactForm() {
05 const [status, setStatus] = useState<"idle" | "loading" | "ok" | "err">("idle");
06
07 async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
08 e.preventDefault();
09 setStatus("loading");
10 const formData = new FormData(e.currentTarget);
11 formData.append("access_key", "YOUR_ACCESS_KEY");
12
13 const res = await fetch("https://splitforms.com/api/submit", {
14 method: "POST",
15 body: formData,
16 });
17 const data = await res.json();
18 setStatus(data.success ? "ok" : "err");
19 if (data.success) e.currentTarget.reset();
20 }
21
22 return (
23 <form onSubmit={handleSubmit}>
24 <input type="text" name="name" required />
25 <input type="email" name="email" required />
26 <textarea name="message" required />
27 <input type="checkbox" name="botcheck" style={{ display: "none" }} tabIndex={-1} />
28 <button type="submit" disabled={status === "loading"}>
29 {status === "loading" ? "Sending…" : "Send"}
30 </button>
31 {status === "ok" && <p>Thanks! We'll be in touch.</p>}
32 {status === "err" && <p>Something went wrong. Try again?</p>}
33 </form>
34 );
35}
1,000
submissions / mo, free
14ms
median latency, edge
0
lines of backend code
17+
frameworks supported
✶ Live preview

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
Next.js contact form on Splitforms — drop-in form backend with spam filtering and webhooks
§ 01Setup3 steps · 60 seconds · zero config

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.

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 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.

snippettsx
"use client";
…
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 Next.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 Next.js form will look like. Submitting opens a confirmation, no real request is sent.

preview · next.jslocalhost:3000
✦ what just happened

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.
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

    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.

  2. 02

    Lock the key to your domains in the Splitforms dashboard so a leaked key can't be replayed elsewhere.

  3. 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.

  4. 04

    Show submit state visually — disabled button + spinner — most users hammer submit on slow networks otherwise.

  5. 05

    Validate email server-side too. Browser type="email" only checks format; spam bots send valid-format throwaway addresses.

§ 04Common gotchas in Next.js6 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 Next.js support ticket at least once.

⚠ gotcha

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.

⚠ gotcha

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.

⚠ gotcha

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.

⚠ gotcha

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.

⚠ gotcha

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.

⚠ gotcha

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.

§ 04bNative Next.js forms…and where they break down

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.

§ 04cAlternative integration patterns2 ways to wire it

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

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-a.tsxtsx10 lines
01// app/actions/contact.ts
02"use server";
03import { redirect } from "next/navigation";
04
05export async function submitContact(formData: FormData) {
06 formData.append("access_key", process.env.SPLITFORMS_KEY!);
07 const res = await fetch("https://splitforms.com/api/submit", { method: "POST", body: formData });
08 if (!(await res.json()).success) throw new Error("Submission failed");
09 redirect("/thanks");
10}
PATTERN B

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.

pattern-b.tsxtsx17 lines
01"use client";
02import { useState } from "react";
03export default function ContactForm() {
04 const [s, setS] = useState<"idle" | "loading" | "ok" | "err">("idle");
05 return (
06 <form onSubmit={async (e) => {
07 e.preventDefault(); setS("loading");
08 const fd = new FormData(e.currentTarget);
09 fd.append("access_key", process.env.NEXT_PUBLIC_SPLITFORMS_KEY!);
10 const r = await fetch("https://splitforms.com/api/submit", { method: "POST", body: fd });
11 setS((await r.json()).success ? "ok" : "err");
12 }}>
13 <input name="email" type="email" required />
14 <button disabled={s === "loading"}>{s === "loading" ? "…" : "Send"}</button>
15 </form>
16 );
17}
§ 04dDeployment notes for Next.jshosting · env vars · CSP

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.

§ 05Comparisonvs native next.js

splitforms vs native next.js.

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

FeatureNative Next.jssplitforms
Setup timeAPI route + nodemailer + DB schema (4-6 hours)60 seconds
Spam filteringDIY honeypot or paid CAPTCHAHoneypot + AI classifier built-in
Email deliverabilityConfigure SPF/DKIM/DMARC yourselfBranded sender configured
Submission storagePostgres/SQLite + RLS policiesDashboard with search/export
Webhook on submitWrite your own dispatcherSlack/Discord/custom, signed
Cost (1k subs/mo)Server + email provider ~$5-25$0 (free tier covers it)
§ 06Alternative patterntsx · 29 lines
ALTERNATIVE

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.

alternative.tsxtsx29 lines
01// app/actions/submit-contact.ts
02"use server";
03import { redirect } from "next/navigation";
04
05export async function submitContact(formData: FormData) {
06 formData.append("access_key", process.env.SPLITFORMS_KEY!);
07
08 const res = await fetch("https://splitforms.com/api/submit", {
09 method: "POST",
10 body: formData,
11 });
12 const data = await res.json();
13 if (!data.success) throw new Error(data.message);
14 redirect("/thanks");
15}
16
17// app/contact/page.tsx
18import { submitContact } from "@/app/actions/submit-contact";
19
20export default function ContactPage() {
21 return (
22 <form action={submitContact}>
23 <input name="name" required />
24 <input name="email" type="email" required />
25 <textarea name="message" required />
26 <button type="submit">Send</button>
27 </form>
28 );
29}
§ 07Questions6 answered

Things developers ask before they integrate.

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

01Does Splitforms work with Next.js App Router?
Yes — App Router is the recommended approach. Use a client component ("use client") with a fetch call, or use a server action that posts FormData to the Splitforms endpoint. Both patterns are documented above.
02Can I use Splitforms with Next.js Pages Router?
Yes. The same React component with useState + fetch works identically in pages/contact.tsx. No App Router-specific features required.
03Should I use NEXT_PUBLIC_SPLITFORMS_KEY or keep the key server-side?
Either works. Public env var is simpler and is fine if you've locked the key to your domain in the Splitforms dashboard (Settings → Security → Allowed domains). Keep it server-side via server action if you want maximum opacity — but a leaked-but-domain-locked key is functionally inert anyway.
04Does the form work without JavaScript?
Yes if you use the server-action pattern (or plain HTML form action pointing at /api/submit and a redirect field for the thank-you page). The fetch-based variant requires JS.
05How do I add file uploads?
File uploads are on the Splitforms roadmap (Q3 2026). Today, file inputs in your Next.js form will be silently dropped. For uploads-required workflows, use a separate file-upload service like UploadThing alongside Splitforms for the metadata.
06Does Splitforms support TypeScript?
Splitforms is API-only — there's no client library to install, just fetch. Your TypeScript code is type-safe naturally. The /api/submit endpoint returns { success: boolean, message?: string } which you can type yourself.
✻ ✻ ✻

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.

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