netlify-forms
netlify/context-and-tools
Serverless form handling for Netlify sites — auto-detect HTML forms, store submissions, filter spam, send notifications.
What is netlify-forms?
Netlify Forms detects HTML forms at deploy time and handles submissions serverlessly, with built-in spam filtering, file uploads, and email notifications. Use it to add contact forms, lead-capture forms, file uploads, or newsletter signups to Netlify-hosted sites, including those built with Next.js, Nuxt, SvelteKit, Astro, or Gatsby.
- Detect HTML forms marked with `data-netlify="true"` at deploy time and register them automatically
- Store form submissions in the Netlify UI and retrieve them via the Netlify API
- Filter spam submissions using Akismet and optional honeypot or reCAPTCHA validation
- Send email notifications to a configurable address with customizable subject lines
- Handle file uploads up to 8 MB per request with multipart form data
- Support AJAX form submission with URL-encoded POST bodies and custom success pages
How to install netlify-forms
npx skills add https://github.com/netlify/context-and-tools --skill netlify-forms- Site must be hosted on Netlify
- Form detection must be enabled once in the Netlify UI (Forms > Enable form detection)
- For JS-rendered or SSR forms, a static skeleton file `public/__forms.html` with form definitions is required
How to use netlify-forms
- 1.Mark your HTML form with `data-netlify="true"` or the `netlify` attribute on the `<form>` tag
- 2.Set a unique `name` attribute on the form and include a hidden `form-name` input for JS-rendered forms
- 3.For AJAX submission, prevent default form submission and POST to `/__forms.html` (SSR) or `/` (static) with URL-encoded body
- 4.Add spam prevention: include a honeypot field with `netlify-honeypot="bot-field"` or enable reCAPTCHA with `data-netlify-recaptcha="true"`
- 5.Set a custom success page with the `action` attribute (e.g., `action="/thank-you"`), or handle success via AJAX callback
- 6.Deploy the site; Netlify will detect and register the form at build time, and submissions will appear in Forms > Submissions
Use cases
- Add a contact form to a static site or framework-based site (Next.js, Nuxt, SvelteKit, Astro, Gatsby)
- Capture leads with a form that stores submissions and sends email notifications
- Implement a newsletter signup with spam filtering and custom thank-you redirect
- Handle file uploads in a form with automatic storage and direct URL access
- Set up AJAX form submission without full-page navigation or backend code
- Netlify site owners and developers
- Frontend developers building contact or lead-capture forms
- Teams using Next.js, Nuxt, SvelteKit, Astro, or Gatsby on Netlify
- Developers who want serverless form handling without managing a backend
netlify-forms FAQ
For JS-rendered, SSR, or framework forms, you must create a static skeleton file at `public/__forms.html` with the form definition and all field names matching exactly. SSR apps must also POST to `/__forms.html`, not `/`. Netlify detects forms only from the final built HTML at deploy time, not from runtime-rendered forms.
No. The request body must be URL-encoded (`application/x-www-form-urlencoded`). JSON is not supported by Netlify Forms.
Add a hidden `subject` input to the form, or configure notifications in the Netlify UI (Forms > Submission notifications > Add notification). The HTML `subject` input always overrides the UI setting. You can use variables like `%{formName}`, `%{siteName}`, and `%{submissionId}` in the subject line.
Maximum 8 MB per request, one file per field. After form deletion, uploaded files remain at their direct URL for 24 hours. For sensitive PII uploads, integrate Very Good Security for extra protection.
Use the Netlify API endpoint `listFormSubmissions` (documented at https://open-api.netlify.com/#tag/submission/operation/listFormSubmissions). Page through results using the `Link` header to avoid silently dropping data. Query spam submissions with `?state=spam`.
Full instructions (SKILL.md)
Source of truth, from netlify/context-and-tools.
name: netlify-forms description: Serverless form handling on Netlify-hosted sites — detects HTML forms at deploy time, stores submissions, filters spam, and sends notifications. Use when adding a contact form, lead-capture form, file-upload form, or newsletter signup to a Netlify site; wiring AJAX form submission; setting up a custom thank-you page; adding a honeypot or reCAPTCHA to a form; getting forms working in Next.js, Nuxt, SvelteKit, Astro, or Gatsby; reading form submissions via the Netlify API; or debugging missing submissions and forms that silently fail to register.
Netlify Forms
Mark a form for detection with data-netlify="true" (or the bare netlify attribute — equivalent) on the <form> tag. Forms are detected by parsing the final built HTML at deploy time — there is no runtime API call or backend code. Client-side/JS-rendered/SSR forms are NOT in the built HTML and are never detected on their own; they require a static skeleton file (see below).
Prerequisite: form detection must be enabled once in the Netlify UI (Forms > Enable form detection). Takes effect on the next deploy.
Static HTML form
<form name="contact" method="POST" data-netlify="true">
<p><label>Your Name: <input type="text" name="name" /></label></p>
<p><label>Your Email: <input type="email" name="email" /></label></p>
<p><label>Message: <textarea name="message"></textarea></label></p>
<p><button type="submit">Send</button></p>
</form>
namesets the form name in the UI and must be unique per site.- At deploy, Netlify strips the
data-netlify/netlifyattribute and injects<input type="hidden" name="form-name" value="contact" />. - Add an
<input name="email">so the notification email'sReply-tois set to the submitter.
JS-rendered / SSR / framework forms (Next.js, Nuxt, SvelteKit, Astro, Gatsby)
Two required pieces:
1. Static skeleton file public/__forms.html — a hidden copy of each form with data-netlify="true", a hidden form-name input, and every field the component submits, with names matching exactly (Netlify validates field names against the registered form). Without this file, submissions silently fail.
<!-- public/__forms.html -->
<form name="pizzaOrder" data-netlify="true" hidden>
<input type="hidden" name="form-name" value="pizzaOrder" />
<input name="order" type="text" />
</form>
2. The rendered form carries a matching hidden form-name input:
<form name="pizzaOrder" method="post" data-netlify="true" onSubmit={handleSubmit}>
<input type="hidden" name="form-name" value="pizzaOrder" />
<input name="order" type="text" onChange={handleChange} />
<input type="submit" />
</form>
⚠️ SSR POST target: In SSR apps, fetch("/") is intercepted by the SSR catch-all function and never reaches form processing. POST to the static skeleton file itself — /__forms.html — not / or an arbitrary path.
⚠️ Astro on-demand routes: Routes with export const prerender = false or output: "server" are never scanned at build time, so their forms are never registered. Put the form on a prerendered page, or rely on the static skeleton file.
Next.js Runtime v5 (Next.js 13.5+): extract form definitions to the static skeleton file and submit via AJAX rather than full-page navigation. See https://docs.netlify.com/build/frameworks/framework-setup-guides/nextjs/overview#v5-breaking-changes
AJAX submission
const handleSubmit = event => {
event.preventDefault();
const formData = new FormData(event.target);
fetch("/__forms.html", { // static sites may POST to "/"; SSR must target the skeleton file
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams(formData).toString()
})
.then(() => alert("Thank you for your submission")) // or navigate("/thank-you")
.catch(error => alert(error));
};
document.querySelector("form").addEventListener("submit", handleSubmit);
- Body MUST be URL-encoded. JSON is NOT supported.
- If the rendered form has no hidden
form-nameinput, you MUST include aform-namefield in the POST body. - The honeypot field name and
g-recaptcha-response(if used) must be in the body — automatic withFormData().
File uploads
Add type="file"; optionally enctype="multipart/form-data" on the <form>. For AJAX file uploads, do NOT set a Content-Type header — let the browser set it (with the multipart boundary).
document.forms.fileForm.addEventListener("submit", event => {
event.preventDefault();
fetch("/", { body: new FormData(event.target), method: "POST" }) // no headers
.then(() => { /* success */ });
});
Limits: one file per field (use multiple fields for multiple files) · 8 MB max request size · 30 s upload timeout · after form deletion, uploaded files stay at their direct URL for 24 h. PII uploads need extra security (Very Good Security integration).
Custom success page
Add an action path relative to site root, starting with /. Use extensionless paths — Netlify serves thank-you.html at /thank-you; the .html path returns 404.
<form name="contact" action="/thank-you" method="POST" data-netlify="true"></form>
Custom success alert is only possible via AJAX (substitute the redirect with your own logic).
Spam prevention
All submissions are filtered by Akismet. Passed → Verified submissions; flagged → Spam submissions. Honeypot/reCAPTCHA failures are rejected and appear in neither list.
Honeypot: add netlify-honeypot="bot-field" to the <form> and include a CSS-hidden field of that name. Any value entered → submission quietly rejected.
<form name="contact" method="POST" netlify-honeypot="bot-field" data-netlify="true">
<p class="hidden"><label>Don’t fill this out: <input name="bot-field" /></label></p>
<!-- real fields -->
</form>
Netlify reCAPTCHA 2: add data-netlify-recaptcha="true" to the <form> AND an empty <div data-netlify-recaptcha="true"></div> where it renders. Only ONE Netlify-provided challenge per page — for multiple, use custom reCAPTCHA. For JS-rendered forms, also add the div to the static skeleton file.
Custom reCAPTCHA 2: your own reCAPTCHA snippet + data-netlify-recaptcha="true" on the <form>, plus env vars:
SITE_RECAPTCHA_KEY— site key (scopes: Builds + Runtime)SITE_RECAPTCHA_SECRET— secret (scope: Runtime)
Email notifications & subject line
Default sender: formresponses@netlify.com. Set subject via a hidden subject input or the Netlify UI (Forms > Submission notifications) — not both; the HTML value always overrides the UI.
<input type="hidden" name="subject" value="New lead from %{formName} (%{submissionId})" />
Variables: %{formName}, %{siteName}, %{submissionId}. Forms created before May 5, 2023 carry a [Netlify] subject prefix — remove it by adding the data-remove-prefix attribute to the subject input.
Set up notifications (email/webhook/Slack) in the UI: Forms > Submission notifications > Add notification.
Reading submissions via the API
Use only documented surfaces. Do NOT invent api.netlify.com endpoints or read tokens from local CLI config files. Reference: https://open-api.netlify.com/#tag/submission/operation/listFormSubmissions
- Page through results using the
Linkheader — code that reads only the first response silently drops the rest. listFormSubmissionsreturns data from old/removed fields no longer shown in the UI.- Query spam with
?state=spam.
Submission summary (field order matters)
The UI summary is derived from field type, not name:
- Title: first non-hidden text
<input>that isn't email-like (type="email", or name matchingemail/mail/from/twitter/sender); falls back to a field namedtitleorsubject. - Body: first
<textarea>.
Field order in the HTML affects what appears in the summary.
Debugging missing submissions
- First suspect: Akismet false positive. A missing legitimate submission is usually spam-flagged — check the Spam list (or API
?state=spam) and mark it verified. Do NOT build a custom recovery function or disable spam filtering as a first resort. - Test submissions get flagged as spam: use a real email (not
test@test.com), write full sentences, don't hammer from one IP. - No submissions at all: confirm form detection is enabled (Forms > Form detection) and redeploy.
- SSR/JS forms silently failing: verify the static skeleton file exists with exactly-matching field names and that AJAX targets the skeleton file, not
/. - Missing old-field data: the UI shows only fields from the last deployed form version. Mark old fields
hiddeninstead of removing them to keep them visible; old data remains available vialistFormSubmissions.
Constraints
- Deleting a form is permanent: future submissions return
404, past submissions become unavailable. Export CSV first. - Submitted code is sanitized (
<script>→ escaped entities). - For PII, export and delete data regularly.
- Data is stored in Netlify's database, not accessible except via UI/API/CSV.
Netlify house rules (forms)
These are org conventions and field-learned guardrails, not docs facts — they are merged into the rendered skill by ctx-gen and are never generated. Extracted from the previous hand-written netlify-forms skill; owned by the skills maintainer.
- In SSR apps (Next.js, Nuxt, SvelteKit, etc.),
fetch("/")is intercepted by the SSR catch-all function and never reaches Netlify's form processing. POST the AJAX submission to the static skeleton file itself (e.g./__forms.html), not to an arbitrary path. - Use only documented surfaces: do not curl
https://api.netlify.com/...with an invented endpoint shape, and do not read tokens out of local CLI config files (~/Library/Preferences/netlify/config.json). - When reading submissions via the API, page through results (
Linkheader); code that reads only the first response silently drops the rest. - For JS-rendered and SSR forms, always create the static skeleton file
public/__forms.html: a hidden copy of each form withdata-netlify="true", a hiddenform-nameinput, and every field the component submits — names matching exactly (Netlify validates field names against the registered form). Without this file, submissions silently fail. - Astro routes rendered on demand (
export const prerender = false, oroutput: "server"routes) are never scanned at build time, so their forms are never registered. Put the form on a prerendered page or rely on the static skeleton file. - A "missing" legitimate submission is usually an Akismet false positive:
check the Spam list (or the API with
?state=spam) and mark it verified. Do not build a custom recovery function or disable spam filtering as a first resort. - For custom success pages, use extensionless
actionpaths (/thank-you, not/thank-you.html) — Netlify servesthank-you.htmlat/thank-youand the.htmlpath returns 404.
Related skills
More from netlify/context-and-tools and the wider catalog.

netlify-frameworks
Deploy and configure web frameworks on Netlify with SSR adapters, build settings, and local platform emulation.

netlify-functions
Write and deploy serverless functions in TypeScript, JavaScript, or Go on Netlify with modern handler APIs.

netlify-identity
Add user authentication to Netlify sites with signup, login, OAuth, and role-based access control.

netlify-image-cdn
Transform images on demand with resizing, cropping, format conversion, and quality control via Netlify Image CDN.

netlify-mcp-servers
Build and deploy secure MCP servers on Netlify Functions to expose your app's tools to AI agents like Claude and Cursor.

context7
Fetch current library docs and API references via Context7 REST API when MCP server is unavailable.