Comparison

Framer contact form component

Two Framer code components. You drop them on the canvas like any other component and set the access key, button label, success text and accent colour in the properties panel. No Embed element and no paid custom code needed.

Short answer: In Framer open Assets › Code › New code file, paste FormgongContact.tsx, then drag the component onto the page and paste your Formgong access key into its Access key property.

Code component or Framer's own form?

Framer's built-in form can send to a webhook, and the Framer contact form page shows how to point that at Formgong. A code component is the other route: you get your own fields and messages, and the success text appears only after Formgong accepts the message.

Contact form component

The @framerSupportedLayout comments let you resize it on the canvas. The text inherits the font you set on the component.

FormgongContact.tsx
import { useState, type CSSProperties, type FormEvent } from "react";
import { addPropertyControls, ControlType } from "framer";

type Props = {
  accessKey: string;
  buttonLabel: string;
  successText: string;
  accent: string;
  style?: CSSProperties;
};

/**
 * @framerSupportedLayoutWidth any
 * @framerSupportedLayoutHeight auto
 */
export default function FormgongContact({ accessKey, buttonLabel, successText, accent, style }: Props) {
  const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle");
  const [notice, setNotice] = useState("");

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const form = event.currentTarget;
    const data = new FormData(form);
    data.set("access_key", accessKey);
    setStatus("sending");
    setNotice("");
    try {
      const response = await fetch("https://formgong.com/submit", {
        method: "POST",
        headers: { Accept: "application/json" },
        body: data,
      });
      const result: { success: boolean; message?: string } = await response.json();
      if (result.success === true) {
        form.reset();
        setStatus("sent");
        setNotice(successText);
      } else {
        setStatus("error");
        setNotice(result.message || "Not sent. Please try again.");
      }
    } catch {
      setStatus("error");
      setNotice("Not sent: check your connection and try again.");
    }
  }

  const field: CSSProperties = { font: "inherit", padding: "10px 12px", border: "1px solid #c7c7cc", borderRadius: 8, width: "100%", boxSizing: "border-box" };
  return (
    <form onSubmit={handleSubmit} style={{ ...style, position: "relative", display: "grid", gap: 12, fontFamily: "Inter, system-ui, sans-serif" }}>
      <input style={field} type="text" name="name" placeholder="Name" aria-label="Name" autoComplete="name" required />
      <input style={field} type="email" name="email" placeholder="Email" aria-label="Email" autoComplete="email" required />
      <textarea style={{ ...field, minHeight: 120 }} name="message" placeholder="Message" aria-label="Message" required />
      <div aria-hidden="true" style={{ position: "absolute", width: 1, height: 1, overflow: "hidden", clipPath: "inset(50%)" }}>
        <input type="text" name="botcheck" tabIndex={-1} autoComplete="off" />
      </div>
      <button type="submit" disabled={status === "sending"} style={{ padding: "12px 16px", border: 0, borderRadius: 8, background: accent, color: "#fff", font: "inherit", fontWeight: 600, cursor: "pointer" }}>
        {status === "sending" ? "Sending…" : buttonLabel}
      </button>
      <p role={status === "error" ? "alert" : "status"} style={{ margin: 0 }}>{notice}</p>
    </form>
  );
}

addPropertyControls(FormgongContact, {
  accessKey: { type: ControlType.String, title: "Access key", defaultValue: "fk_your_access_key" },
  buttonLabel: { type: ControlType.String, title: "Button", defaultValue: "Send message" },
  successText: { type: ControlType.String, title: "Success text", defaultValue: "Thanks! Your message has been sent." },
  accent: { type: ControlType.Color, title: "Accent", defaultValue: "#0f62fe" },
});

Newsletter signup component

Email and button in one row, with a hidden _subject so the notification says "New newsletter signup".

FormgongNewsletter.tsx
import { useState, type CSSProperties, type FormEvent } from "react";
import { addPropertyControls, ControlType } from "framer";

type Props = { accessKey: string; buttonLabel: string; successText: string; accent: string; style?: CSSProperties };

/**
 * @framerSupportedLayoutWidth any
 * @framerSupportedLayoutHeight auto
 */
export default function FormgongNewsletter({ accessKey, buttonLabel, successText, accent, style }: Props) {
  const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle");
  const [notice, setNotice] = useState("");

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const form = event.currentTarget;
    const data = new FormData(form);
    data.set("access_key", accessKey);
    setStatus("sending");
    setNotice("");
    try {
      const response = await fetch("https://formgong.com/submit", {
        method: "POST",
        headers: { Accept: "application/json" },
        body: data,
      });
      const result: { success: boolean; message?: string } = await response.json();
      if (result.success === true) {
        form.reset();
        setStatus("sent");
        setNotice(successText);
      } else {
        setStatus("error");
        setNotice(result.message || "Not sent. Please try again.");
      }
    } catch {
      setStatus("error");
      setNotice("Not sent: check your connection and try again.");
    }
  }

  return (
    <form onSubmit={handleSubmit} style={{ ...style, position: "relative", display: "flex", flexWrap: "wrap", gap: 8, fontFamily: "Inter, system-ui, sans-serif" }}>
      <input type="hidden" name="_subject" value="New newsletter signup" />
      <input type="email" name="email" placeholder="you@example.com" aria-label="Email" autoComplete="email" required
        style={{ flex: "1 1 12rem", font: "inherit", padding: "10px 12px", border: "1px solid #c7c7cc", borderRadius: 8 }} />
      <div aria-hidden="true" style={{ position: "absolute", width: 1, height: 1, overflow: "hidden", clipPath: "inset(50%)" }}>
        <input type="text" name="botcheck" tabIndex={-1} autoComplete="off" />
      </div>
      <button type="submit" disabled={status === "sending"} style={{ padding: "10px 16px", border: 0, borderRadius: 8, background: accent, color: "#fff", font: "inherit", fontWeight: 600 }}>
        {status === "sending" ? "…" : buttonLabel}
      </button>
      <p role={status === "error" ? "alert" : "status"} style={{ flexBasis: "100%", margin: 0 }}>{notice}</p>
    </form>
  );
}

addPropertyControls(FormgongNewsletter, {
  accessKey: { type: ControlType.String, title: "Access key", defaultValue: "fk_your_access_key" },
  buttonLabel: { type: ControlType.String, title: "Button", defaultValue: "Subscribe" },
  successText: { type: ControlType.String, title: "Success text", defaultValue: "You're subscribed. Thanks!" },
  accent: { type: ControlType.Color, title: "Accent", defaultValue: "#0f62fe" },
});

How we tested this code

We pasted each snippet unchanged into a fresh project (React 19.3, rendered with the same props Framer passes from the property controls), filled in a real access key, and submitted it in Chromium three times against a local copy of Formgong. With the right key the form showed its success text and exactly one submission was stored, not marked as spam. With a key Formgong does not know, the form showed Formgong's own error, "That access key does not exist.", and nothing was stored. With the request cut off, the form said it was not sent. None of the snippets says "sent" before Formgong answers success: true.

Framer's runtime is not available outside Framer, so a two-line stand-in replaced addPropertyControls and ControlType. Everything else ran as written. Send one test from your own Framer project.

Questions

Is it safe to put the access key in front-end code?

Yes. The key only lets someone send a submission to your form, the same thing your page already does. It cannot read submissions or change settings. If you want it out of the page source anyway, add your domain under allowed domains in the form settings, or post through your own server.

Where do the messages go?

To your Formgong inbox and, by default, to the email you signed up with. In the form settings you can add more recipients, Telegram, Slack or a Google Sheet.

Can I add more fields?

Copy one of the input lines and give it a new name. Formgong stores any field name it receives, so nothing changes on the Formgong side.

What is the hidden botcheck field?

A honeypot. It sits off screen, so people never fill it, while simple bots fill every field they find. Formgong marks a submission with it filled in as spam, so it does not reach your email. Keep the name botcheck.

Sources

Checked on 16.10.2026 against these public pages: Framer: Code components, Framer: Property controls.

← Formgong