splitforms.com
FRAMER · CONTACT FORM

Contact form for Framer websites

Framer's built-in forms send to a single email, gate webhooks behind paid plans, and limit free submissions. Paste a Code Component instead — same canvas drag-and-drop, real backend, 1,000 submissions/month free, signed webhooks, dashboard search.

1,000 free submissions every month.·No credit card.
contact.tsxtsx41 lines
01// ContactForm.tsx — paste into Framer's Code Components panel
02// Insert the resulting component into any frame on the canvas.
03import { useState } from "react";
04import { addPropertyControls, ControlType } from "framer";
05
06export default function ContactForm({ accessKey = "YOUR_ACCESS_KEY" }) {
07 const [status, setStatus] = useState<"idle" | "loading" | "ok" | "err">("idle");
08
09 async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
10 e.preventDefault();
11 setStatus("loading");
12 const formData = new FormData(e.currentTarget);
13 formData.append("access_key", accessKey);
14
15 const res = await fetch("https://splitforms.com/api/submit", {
16 method: "POST",
17 body: formData,
18 });
19 const data = await res.json();
20 setStatus(data.success ? "ok" : "err");
21 if (data.success) e.currentTarget.reset();
22 }
23
24 return (
25 <form onSubmit={handleSubmit} style={{ display: "grid", gap: 12 }}>
26 <input name="name" placeholder="Name" required />
27 <input name="email" type="email" placeholder="Email" required />
28 <textarea name="message" placeholder="Message" required />
29 <input type="checkbox" name="botcheck" style={{ display: "none" }} tabIndex={-1} />
30 <button type="submit" disabled={status === "loading"}>
31 {status === "loading" ? "Sending…" : "Send"}
32 </button>
33 {status === "ok" && <p>Thanks!</p>}
34 {status === "err" && <p>Something went wrong.</p>}
35 </form>
36 );
37}
38
39addPropertyControls(ContactForm, {
40 accessKey: { type: ControlType.String, title: "Access Key" },
41});
1,000
submissions / mo, free
14ms
median latency, edge
0
lines of backend code
17+
frameworks supported
✶ Live preview

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

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

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

snippettsx
// ContactForm.tsx — paste into Framer's Code Components panel
…
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 Framer 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 Framer form will look like. Submitting opens a confirmation, no real request is sent.

preview · framerlocalhost:3000
✦ what just happened

Your Framer 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 `addPropertyControls` to expose the access key as a property — don't hardcode it. Then designers can swap keys per page or per environment without touching code.

  2. 02

    Style the form with inline `style` objects, not CSS modules. Framer's bundling doesn't transform CSS imports inside Code Components.

  3. 03

    Set `display: 'grid', gap: 12` on the form for consistent spacing — Framer's flex defaults can fight your form layout.

  4. 04

    Lock the access key to your `*.framer.app` AND custom domain in the splitforms dashboard. Framer's preview URL is a different origin than your published site.

  5. 05

    If you have multiple forms on different pages, give each a unique `form-name` hidden field so the splitforms dashboard groups submissions correctly.

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

⚠ gotcha

addPropertyControls must be the LAST statement in the file

Framer reads property controls only if addPropertyControls(Component, {...}) is called after the component is exported. If you put it before the export or wrap it in a conditional, the access-key control disappears from the right panel and you can't set the key visually.

⚠ gotcha

Framer's canvas re-renders the component on every prop change

Editing the access key in the right panel re-mounts the form mid-edit. If a user is testing the form when you change the key, their status === 'loading' state resets to 'idle' visually but the in-flight fetch still completes. Not a bug in production — only an editor quirk.

⚠ gotcha

Framer's published page CSP blocks splitforms.com unless you set it

Framer's published sites have a default Content Security Policy that allows known integrations. Splitforms isn't on the default allowlist — you may need to add connect-src https://splitforms.com via Framer's Site Settings → Custom Code → Head HTML, inside a <meta http-equiv="Content-Security-Policy" content="…"> tag.

⚠ gotcha

Code Components don't get Framer's font system by default

If you use style={{ fontFamily: '...' }} in a Code Component, you have to import the font manually — Framer's site fonts only auto-apply to canvas-built elements. Use font-family: inherit to inherit from the parent frame.

⚠ gotcha

Form fields don't focus visibly without explicit styling

Browser-default focus rings are sometimes hidden by Framer's reset CSS. Add :focus { outline: 2px solid currentColor; outline-offset: 2px; } in your component's styles — accessibility requires a visible focus indicator.

⚠ gotcha

Framer's Smart Components break Code Component state on layout swap

If your contact form Code Component lives inside a Smart Component variant (e.g. a 'Mobile' variant of a header), Framer re-instantiates the entire Code Component when switching variants — losing your useState for status. A user mid-submission who rotates their device sees the form reset to 'idle' while the fetch is still in flight, then a stale success/error notification when the request resolves into a now-unmounted component (React logs a 'state update on unmounted component' warning). Either lift the form out of any Smart Component variant, or persist status to sessionStorage and rehydrate on mount.

§ 04bNative Framer forms…and where they break down

How Framer handles forms without splitforms.

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

Framer's built-in form widget delivers submissions to a single email address (configured per form) with no dashboard for managing submissions, no webhooks below the Pro plan, and no spam filtering beyond Framer's basic bot detection. CMS-driven Framer forms inherit the same constraints. The native flow works for a portfolio's contact form; it falls apart for any setup that needs Slack/Discord notifications, multiple recipients, CSV export, or tagged form-name routing. Replacing it means writing a Code Component — Framer's mechanism for embedding custom React. That's the pattern splitforms uses: a Code Component with the access key as a property control, dropped onto the canvas like a native widget.

§ 04cAlternative integration patterns2 ways to wire it

Two ways to ship splitforms on Framer.

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

PATTERN A

Pattern A — Code Component with property control

Paste into Framer's Code Components panel. The access key becomes a property in the right-side inspector — designers swap keys per page or per environment without touching code.

pattern-a.tsxtsx19 lines
01// ContactForm.tsx (Framer Code Component)
02import { useState } from "react";
03import { addPropertyControls, ControlType } from "framer";
04export default function ContactForm({ accessKey = "YOUR_ACCESS_KEY" }) {
05 const [status, setStatus] = useState<"idle" | "loading" | "ok" | "err">("idle");
06 return (
07 <form onSubmit={async (e) => {
08 e.preventDefault(); setStatus("loading");
09 const fd = new FormData(e.currentTarget);
10 fd.append("access_key", accessKey);
11 const r = await fetch("https://splitforms.com/api/submit", { method: "POST", body: fd });
12 setStatus((await r.json()).success ? "ok" : "err");
13 }} style={{ display: "grid", gap: 12 }}>
14 <input name="email" type="email" required />
15 <button disabled={status === "loading"}>Send</button>
16 </form>
17 );
18}
19addPropertyControls(ContactForm, { accessKey: { type: ControlType.String } });
PATTERN B

Pattern B — Framer override (apply to existing canvas form)

If you already designed a form on the canvas with Framer's native form widget, an override can intercept its submit and re-route to splitforms. Useful when you want to keep the canvas-built design but swap the backend.

pattern-b.tsxtsx12 lines
01// SplitformsOverride.tsx
02import type { ComponentType } from "react";
03export function withSplitforms(Component): ComponentType {
04 return (props) => (
05 <Component {...props} onSubmit={async (e) => {
06 e.preventDefault();
07 const fd = new FormData(e.currentTarget);
08 fd.append("access_key", "YOUR_ACCESS_KEY");
09 await fetch("https://splitforms.com/api/submit", { method: "POST", body: fd });
10 }} />
11 );
12}
§ 04dDeployment notes for Framerhosting · env vars · CSP

Shipping Framer + splitforms to production.

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

Framer publishes sites to its own CDN — there's no Vercel/Netlify config to manage. The form posts cross-origin to splitforms regardless. Framer's published sites have a default Content Security Policy that may block connect-src to splitforms.com on some plans; if submissions silently fail, add <meta http-equiv="Content-Security-Policy" content="connect-src https://splitforms.com"> via Site Settings → Custom Code → Head HTML. Lock the access key to BOTH your *.framer.app preview URL AND your custom domain — Framer serves both with different Origin headers. Framer's preview environment runs Code Components live, so you can test the form before publishing.

§ 05Comparisonvs native framer

splitforms vs native framer.

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

FeatureNative Framersplitforms
Free monthly submissionsLimited on free plan1,000 (any Framer plan)
Spam filterFramer's basicHoneypot + classifier
WebhooksPro tier and upFree, signed payloads
Custom redirectYesYes (hidden input)
Submission dashboardFramer projectsSplitforms dashboard, exportable
Backend costBundled with Framer planFree (separate from Framer plan)
§ 07Questions6 answered

Things developers ask before they integrate.

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

01How do I add a custom contact form to Framer?
Open the Code Components panel in Framer (sidebar → Components → New Code Component). Paste the snippet above. Save. The component appears in the Insert menu — drag it onto any frame. Set the accessKey property in the right panel.
02Does splitforms work with Framer's published sites?
Yes. Framer's published sites are static React + JS, and Code Components run in the browser. The form posts directly to splitforms.com — no Framer-side configuration beyond the Code Component itself.
03How do I handle form errors in Framer?
Use a status state with four values (idle/loading/ok/err) inside the Code Component. Render error messages from the splitforms response. Style them with inline styles since Framer's CSS modules don't apply inside Code Components.
04Can I use splitforms with Framer's built-in form widget too?
Not directly — Framer's built-in form widget posts to Framer's backend. Replace it with the Code Component to use splitforms. The Code Component renders inside Framer just like a native widget.
05How do I customize the success / redirect behavior?
Two options. (1) Inline success message: render a <p> when status === 'ok' (default in our snippet). (2) Redirect: add a hidden <input name="redirect" value="https://yoursite.framer.app/thanks"> and use e.currentTarget.submit() instead of fetch — splitforms 302s the browser there.
06Does this work in Framer's preview / does it work with Framer Sites and Framer Web?
Yes — both. Framer's preview executes Code Components (so you can test the form in the editor), and the published site embeds them as React components. Splitforms-side, lock the access key to both your *.framer.app domain and your custom domain.
✻ ✻ ✻

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