To connect a Framer site to FormSubmit, add a code component that posts FormData to your endpoint. Framer code components are React, so the same pattern as any React form applies.
Steps
- Create a FormSubmit form and copy the endpoint.
- In Framer, open Assets → Code → New component and name it
ContactForm. - Replace the file contents with the component below and update
ENDPOINT. - Drag it onto your page, size it, and publish.
// Framer: Assets → Code → New component, paste this, then drag it onto the canvas.
import { useState, type FormEvent } from "react";
const ENDPOINT = "https://formsubmit.app/f/YOUR_FORM_ID";
export default function ContactForm() {
const [status, setStatus] = useState<"idle" | "sending" | "success" | "error">("idle");
const [error, setError] = useState("");
const [renderedAt] = useState(() => Date.now());
async function onSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus("sending");
const data = new FormData(e.currentTarget);
data.set("_ts", String(renderedAt)); // helps FormSubmit's time-based spam check
try {
const res = await fetch(ENDPOINT, {
method: "POST",
body: data,
headers: { Accept: "application/json" },
});
const json = await res.json();
if (!res.ok || !json.ok) throw new Error(json.error?.message ?? "Something went wrong");
setStatus("success");
e.currentTarget.reset();
} catch (err) {
setError((err as Error).message);
setStatus("error");
}
}
if (status === "success") {
return <p className="text-green-700">Thanks! We'll be in touch soon.</p>;
}
return (
<form onSubmit={onSubmit} className="mx-auto max-w-lg space-y-5">
<div className="space-y-1.5">
<label htmlFor="fs-name" className="block text-sm font-medium text-gray-900">Name</label>
<input id="fs-name" type="text" name="name" placeholder="Jane Doe" required className="block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20" />
</div>
<div className="space-y-1.5">
<label htmlFor="fs-email" className="block text-sm font-medium text-gray-900">Email</label>
<input id="fs-email" type="email" name="email" placeholder="jane@example.com" required className="block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20" />
</div>
<div className="space-y-1.5">
<label htmlFor="fs-message" className="block text-sm font-medium text-gray-900">Message</label>
<textarea id="fs-message" name="message" rows={5} placeholder="How can we help?" required className="block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20"></textarea>
</div>
{/* Honeypot: bots fill this in, humans never see it */}
<input type="text" name="_gotcha" tabIndex={-1} autoComplete="off" style={{ display: "none" }} />
{status === "error" && <p className="text-sm text-red-600">{error}</p>}
<button type="submit" disabled={status === "sending"} className="inline-flex w-full items-center justify-center rounded-lg bg-gray-900 px-4 py-2.5 text-sm font-medium text-white hover:bg-gray-800 disabled:opacity-50">
{status === "sending" ? "Sending…" : "Send"}
</button>
</form>
);
}Styling
The generated component uses Tailwind-style class names; Framer doesn't ship Tailwind, so replace them with inline style objects or a small <style> tag, or build the form visually in the form generator and choose the plain HTML + CSS export as a starting point.
Protect the form
Add your *.framer.app preview domain and custom domain to the allowlist.