Guide

Connect a React or Next.js form to a backend

Keep your visitor on the page and control the success state yourself. Post the form as JSON, read the response, and render your own confirmation. No backend code to deploy.

The component

A controlled submit that posts JSON and reacts to the result. Replace YOUR_FORM_ID with your endpoint id:

import { useState } from "react";

export function ContactForm() {
  const [status, setStatus] = useState("idle");

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus("sending");
    const form = e.currentTarget;
    const data = Object.fromEntries(new FormData(form));

    const res = await fetch("https://forms.scrolltheory.media/f/YOUR_FORM_ID", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Accept": "application/json"
      },
      body: JSON.stringify(data)
    });

    const result = await res.json();
    setStatus(result.ok ? "sent" : "error");
  }

  if (status === "sent") return <p>Thanks, we got your message.</p>;

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
      <button disabled={status === "sending"}>
        {status === "sending" ? "Sending..." : "Send"}
      </button>
      {status === "error" && <p>Something went wrong. Please try again.</p>}
    </form>
  );
}

How the response works

Because the request sets Accept: application/json, the endpoint replies with JSON instead of redirecting. A success looks like { ok: true, id, spam, limited }. The spam flag means the submission was quarantined, and limited means the account is over its monthly cap so no email was sent. Either way the submission is stored, so you never lose data. A failure returns { ok: false, error }.

Cross-origin and frameworks

Cross-origin posts are supported, so this works from any domain and from Next.js, Remix, Astro islands, Vite, or plain React. The endpoint does not care about your framework. If you prefer a server action or route handler, post to the same endpoint from the server instead of the browser; the contract is identical.

Full field and control-field reference is in the docs.

Get your endpoint

Create a form on the free plan and you get an endpoint to drop into any of the examples above. Every submission is stored and spam is quarantined rather than deleted, so a false positive never costs you a real lead.

Start free   Read the docs

More guides