splitforms.com
GATSBY · CONTACT FORM

Contact form for Gatsby websites

Gatsby is React under the hood, so any React form works — but the build is static. Use the splitforms endpoint to skip Netlify Forms (which limit submissions on free tier), Lambdas, Gatsby Functions, and AWS API Gateway entirely. One React component, 1,000 free submissions per month, dashboard included.

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

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

Ship a Gatsby 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 Gatsby code

Copy the Gatsby snippet on the right and paste it into your project. Replace YOUR_ACCESS_KEY with the key from step 1.

snippetjsx
import React, { useState } from "react";
…
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 Gatsby 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 Gatsby form will look like. Submitting opens a confirmation, no real request is sent.

preview · gatsbylocalhost:3000
✦ what just happened

Your Gatsby 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

    Use `GATSBY_SPLITFORMS_KEY` in your `.env.production` and `.env.development` files. Gatsby reads them at build time and inlines the value into the static JS bundle — that's expected, lock the key to your domain.

  2. 02

    Don't wrap the form in `<Link>` or any component that intercepts navigation. Use a normal `<form>` element with onSubmit.

  3. 03

    If you have multiple forms (contact, demo, newsletter), include a `form-name` hidden field per form so the splitforms dashboard groups them.

  4. 04

    Build the /thanks page as a real Gatsby page (`src/pages/thanks.js`). The redirect from splitforms uses the URL — Gatsby serves it as static HTML.

  5. 05

    For Gatsby v5 partial hydration, mark the form file with `'use client'` so React knows to hydrate the onSubmit handler on the client.

§ 04Common gotchas in Gatsby6 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 Gatsby support ticket at least once.

⚠ gotcha

GATSBY_ prefix required for env vars exposed at build time

Gatsby's webpack config only exposes process.env.* variables prefixed with GATSBY_. If you write process.env.SPLITFORMS_KEY, you'll get undefined in the browser bundle. Rename to GATSBY_SPLITFORMS_KEY — and accept that it's bundled into the static JS (lock the key to your domain in the splitforms dashboard).

⚠ gotcha

Gatsby's <Link> can't wrap a form's submit handler

Gatsby's <Link> component prevents default navigation. If you wrap your form in <Link to="/thanks"> thinking the redirect will fire, it won't — the form's submit event runs, but the navigation is suppressed. Use a hidden redirect input on the form and let splitforms handle the 302.

⚠ gotcha

SSR + client hydration mismatch on form initial state

If you use useState('idle') in your form and render any state-dependent UI on first paint, Gatsby's static HTML and React's client render can diverge — you'll see a hydration warning. Render the form unconditionally; only render status messages inside the handler-triggered branches.

⚠ gotcha

Gatsby v5 partial hydration changed how forms hydrate

Gatsby 5 introduced partial hydration via React Server Components. If your form is in a Server Component, the onSubmit handler won't bind. Add 'use client' at the top of the file (or use a separate ContactForm.client.jsx).

⚠ gotcha

@reach/router (legacy) intercepts form submits with onSubmit handlers

Older Gatsby setups used @reach/router, which has a known issue where it sometimes intercepts form submissions. If you see your form not POSTing, ensure you've migrated to Gatsby v4+ (uses its own router) or wrap the form in a normal <div> not <Router>.

⚠ gotcha

gatsby-plugin-offline caches the contact page and serves a stale form

If your site uses gatsby-plugin-offline (or the older gatsby-plugin-manifest with service worker registration), the contact page HTML is cached aggressively. After you rotate your GATSBY_SPLITFORMS_KEY and redeploy, returning visitors keep submitting against the old key from the cached bundle until the service worker fetches a fresh shell — sometimes days. Fix: bump the SW cache version in gatsby-config.js on key rotation, or skip the offline plugin on the contact route via runtimeCaching rules. Alternatively, switch to gatsby-plugin-pwa which handles versioned cache invalidation per-page.

§ 04bNative Gatsby forms…and where they break down

How Gatsby handles forms without splitforms.

The shape of the problem before splitforms enters the picture — and the gap it fills for Gatsby specifically.

Gatsby builds a static React app, so 'native' means choosing between (a) Netlify Forms (Gatsby-on-Netlify only, 100 free submissions/month, gatsby-plugin-netlify required), (b) Gatsby Functions (deprecated in Gatsby 5 — they were removed when Gatsby Cloud shut down), or (c) a third-party form backend. Gatsby v4 had Functions running as Lambda-equivalent serverless routes; v5 removed them entirely. Result: every Gatsby contact form today uses an external service. Splitforms is a drop-in replacement — same shape as Netlify Forms (POST to a URL), same shape as Formspree, but with 5× the free monthly submissions and built-in spam filtering.

§ 04cAlternative integration patterns2 ways to wire it

Two ways to ship splitforms on Gatsby.

Pick the pattern that matches your constraints — JS budget, key-exposure tolerance, server-side opacity. Both produce the same result.

PATTERN A

Pattern A — React component (Gatsby v4/v5)

Standard React function component, useState for status. Drop into src/components/ContactForm.jsx and import on any page. Set GATSBY_SPLITFORMS_KEY in .env.production and .env.development.

pattern-a.jsxjsx16 lines
01import React, { useState } from "react";
02export default function ContactForm() {
03 const [status, setStatus] = useState("idle");
04 return (
05 <form onSubmit={async (e) => {
06 e.preventDefault(); setStatus("loading");
07 const fd = new FormData(e.target);
08 fd.append("access_key", process.env.GATSBY_SPLITFORMS_KEY);
09 const r = await fetch("https://splitforms.com/api/submit", { method: "POST", body: fd });
10 setStatus((await r.json()).success ? "ok" : "err");
11 }}>
12 <input name="email" type="email" required />
13 <button disabled={status === "loading"}>Send</button>
14 </form>
15 );
16}
PATTERN B

Pattern B — partial hydration (Gatsby v5)

Gatsby 5's partial hydration mounts only the components marked client-bound. Add 'use client' at the top of the form file so the onSubmit handler binds at hydration time; the rest of the page stays static.

pattern-b.jsxjsx7 lines
01// src/components/ContactForm.jsx
02"use client";
03import React, { useState } from "react";
04export default function ContactForm() {
05 const [status, setStatus] = useState("idle");
06 // ...same body as Pattern A
07}
§ 04dDeployment notes for Gatsbyhosting · env vars · CSP

Shipping Gatsby + splitforms to production.

Host-specific gotchas, env-var conventions, and the boring-but-load-bearing details for putting this on the public internet.

Gatsby builds static HTML + JS that deploys to any host: Netlify, Vercel, Cloudflare Pages, AWS Amplify, S3 + CloudFront, Gatsby Cloud (sunset 2024 — migrate). The form posts cross-origin to splitforms regardless of host. Env vars exposed to the browser bundle must be prefixed GATSBY_ — anything else is undefined client-side. The key gets bundled into the JS at build time; lock it to your domain in the splitforms dashboard. For headless Gatsby + WordPress / Contentful setups, the form lives in the React tree, not the CMS — no special CMS wiring required.

§ 05Comparisonvs native gatsby

splitforms vs native gatsby.

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

FeatureNative Gatsbysplitforms
Setup timeGatsby Function + email + spam (1 day)60 seconds
Free monthly submissions100 (Netlify Forms free)1,000
Spam filterNetlify's honeypot onlyHoneypot + classifier
Build & deploy hostNetlify-locked for Netlify FormsAny host (Vercel, Cloudflare, S3)
Submission storageNetlify dashboardSplitforms dashboard + email
WebhooksNetlify Pro tierFree, signed
§ 07Questions6 answered

Things developers ask before they integrate.

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

01How do I add a contact form to Gatsby?
Drop the React component above into src/components/ContactForm.jsx, set GATSBY_SPLITFORMS_KEY in your .env, and import the component on any page. No plugin, no gatsby-config edits required.
02Does splitforms work with Gatsby v4 and v5?
Yes — both. The component is a standard React function component, no Gatsby-specific APIs. For v5 partial hydration, add 'use client' at the top of the file so the onSubmit handler binds on the client.
03How do I handle form errors in Gatsby?
Use a status state with four values: idle, loading, ok, err. Render error messages from data.message inside an {status === 'err' && …} block. The fetch returns { success, message? }.
04Can I use splitforms with Gatsby Functions or Netlify Functions instead?
You can, but you don't need to. The whole point of splitforms is replacing the function. If you do want a server-side proxy (to hide the key entirely), POST from a Gatsby Function and forward to splitforms — same backend.
05How do I customize the success / redirect behavior?
Two options. (1) Stay on-page with a React-rendered success message (default in our snippet). (2) Add <input type="hidden" name="redirect" value="/thanks" /> and use a non-AJAX form — splitforms 302s after submit. Build /thanks as a normal Gatsby page.
06Will this work with Gatsby Cloud / Netlify / Vercel / Cloudflare Pages?
Yes — every host. The form posts to splitforms.com from the static page, regardless of where the static files are served from. Splitforms's allowed-domains list just needs to include your live URL.
✻ ✻ ✻

Ship your Gatsby 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