RG Forms logoRG Forms
Netlify

Contact forms on Netlify: the built-in option and the portable one

Netlify Forms deserves credit: it’s one of the neatest features on any static host. You add an attribute, Netlify’s build parses your HTML, and submissions start appearing in your dashboard. For a lot of sites that is exactly the right answer, and this page isn’t going to pretend otherwise.

But it’s worth understanding the trade you’re making, because the convenience comes from being deeply tied to the platform.

Last updated August 19, 2026

The short answer

Netlify has a built-in form handler: add a netlify attribute to your form and Netlify's build step detects it and starts capturing submissions into your site dashboard, with no code at all. It's the fastest option if you're staying on Netlify. An external endpoint like RG Forms is the better fit when you want submissions stored in your own Google Sheet rather than a host dashboard, when the site may move hosts later, or when the same form code needs to run identically across several projects — because a plain fetch() has no host-specific build magic behind it.

Two good options, different shapes

Netlify FormsRG Forms
SetupAdd a netlify attribute; detected at buildSign in with Google; POST to your endpoint
Where submissions liveYour Netlify site dashboardA Google Sheet in your own Drive
CostIncluded, with a monthly submission allowance per plan — see Netlify’s pricingFree; no infrastructure to bill for
If you move hostsForm handling stops; export firstNothing changes — the endpoint isn’t tied to a host
Spam handlingBuilt-in filtering, plus honeypot and captcha optionsHoneypot, plus optional Cloudflare Turnstile verified server-side
Works locally / on any previewOnly on deployed Netlify buildsAnywhere, including localhost

Neither of these is the “right” answer in general. Pick on data custody and portability, not on features — both cover the basics well.

When Netlify Forms is the better choice

You’re staying on Netlify, you want zero JavaScript, and you want submissions in the same dashboard as your deploys. It also handles a plain non-JS form post with a redirect, which is genuinely useful if you care about the no-JavaScript case.

When an external endpoint fits better

The site might move. Agency work gets migrated. When it does, host-native forms are the piece that quietly breaks, and the submission history has to be exported before the account is closed.

The client should own the data. Handing over a Google Sheet in the client’s own Drive is a cleaner boundary than sharing access to your Netlify account, and it doesn’t depend on your billing relationship continuing.

You want it working in local dev. Netlify’s form detection happens at deploy time, so you can’t exercise the real path on localhost without a deploy. A fetch to an endpoint behaves identically everywhere.

You maintain many sites. One integration pattern that’s identical across every project — regardless of host or framework — is worth a lot when you’re maintaining a dozen of them.

Wiring up an external endpoint on Netlify

  1. Create the RG Forms project

    Sign in with Google, define your fields, and copy the endpoint URL.

  2. Store the URL as an environment variable (optional)

    In Site configuration → Environment variables, add e.g. VITE_FORM_ENDPOINT or PUBLIC_FORM_ENDPOINT depending on your framework’s prefix convention. This isn’t about secrecy — the URL ends up in the client bundle regardless — it’s so a rebuild can repoint the form without a code change.

  3. Add the form

    Note there’s no netlify attribute and no data-netlify — you’re deliberately opting out of the build-time detection.

    index.html
    <form id="contact-form">
      <label>Name <input name="name" required /></label>
      <label>Email <input type="email" name="email" required /></label>
      <label>Message <textarea name="message" required></textarea></label>
    
      <!-- Honeypot: humans never see it, bots fill it in -->
      <input type="text" name="_hp" tabindex="-1" autocomplete="off"
             style="position:absolute;left:-9999px" aria-hidden="true" />
    
      <button type="submit">Send</button>
      <p id="form-status" role="status"></p>
    </form>
    
    <script>
      const ENDPOINT = "https://script.google.com/macros/s/AKfycb.../exec";
      const form = document.getElementById("contact-form");
      const status = document.getElementById("form-status");
    
      form.addEventListener("submit", async (event) => {
        event.preventDefault();
        status.textContent = "Sending…";
    
        const res = await fetch(ENDPOINT, {
          method: "POST",
          // text/plain avoids the CORS preflight Apps Script can't answer
          headers: { "Content-Type": "text/plain" },
          body: JSON.stringify({
            tab: "contact",
            fields: Object.fromEntries(new FormData(form)),
          }),
        });
    
        const data = await res.json();
        status.textContent =
          data.result === "success" ? "Thanks — we'll be in touch." : "Something went wrong.";
        if (data.result === "success") form.reset();
      });
    </script>
  4. Deploy and submit a real message

    Confirm the row lands in your Sheet. If you’d rather test before deploying, hit the endpoint directly.

    Test the endpoint from your terminal
    curl -L -X POST "https://script.google.com/macros/s/AKfycb.../exec" \
      -H "Content-Type: text/plain" \
      -d '{"tab":"contact","fields":{"name":"Ada","email":"ada@example.com","message":"Hello"}}'
    
    # → {"result":"success"}   and a new row appears in your Google Sheet

Common questions

Will Netlify try to capture my fetch-based form too?

No. Netlify’s build step looks for forms marked with the netlify attribute. Without it, your form is just markup and Netlify ignores it.

Do I need a Netlify Function for this?

No — that’s the point. The browser POSTs straight to your Apps Script endpoint, so there’s no function to write, deploy, or count against your invocation limits.

Does it work on Netlify deploy previews?

Yes, on every preview and branch deploy, and on localhost too. There’s no build-time detection involved.

What about Netlify’s spam filtering?

That applies to Netlify Forms. For an external endpoint you use the endpoint’s own protection: the hidden _hp honeypot, plus Cloudflare Turnstile if you want a server-side captcha check before rows are written.

More answers in the full FAQ.

Your endpoint goes live in ~90 seconds