Eleventy contact forms, zero client JavaScript.
11ty ships no framework to the browser, and your contact form doesn't need one. A plain form POSTs to one endpoint, and every submission becomes an email in your inbox and a record in your dashboard — while your site stays the fast, script-free HTML you built it to be.
An include, a data file, two Markdown pages.
The pieces land where 11ty expects them: the form in _includes, the key in _data, the pages as Markdown with permalinks. The third tab adds an optional fetch upgrade for an inline status message.
{# _includes/contact-form.njk — render it anywhere with
{% include "contact-form.njk" %} #}
<form action="https://api.formwire.com/submit" method="POST">
<input type="hidden" name="access_key" value="{{ formwire.accessKey }}">
{# A static build can't redirect after a POST, so FormWire does it.
Absolute URL — the endpoint sends the visitor back to your site. #}
<input type="hidden" name="_redirect" value="{{ formwire.thanksUrl }}">
<label>
Name
<input type="text" name="name" required>
</label>
<label>
Email
<input type="email" name="email" required>
</label>
<label>
Message
<textarea name="message" required></textarea>
</label>
<button type="submit">Send</button>
</form>
// ─────────────────────────────────────────────────────────────────────
// _data/formwire.js — global data: every template, any engine, can read
// {{ formwire.accessKey }}. The key is public by design, safe to commit.
module.exports = {
accessKey: "YOUR-ACCESS-KEY",
thanksUrl: "https://yoursite.com/thanks/",
}; Swap YOUR-ACCESS-KEY for your form's key in _data/formwire.js. The full field and response reference lives in the docs at /docs/.
The four things that trip up 11ty forms.
Small conventions with outsized consequences. The snippets above already account for all four.
Markdown renders through Liquid by default
Eleventy preprocesses .md files with a template engine, and out of the box that engine is Liquid — so a Nunjucks {% include %} in your contact page fails until you set markdownTemplateEngine: "njk" in the config. One line, but invisible until you know to look for it.
Four spaces of indentation break the form
Raw HTML in Markdown passes through untouched — unless it's indented four or more spaces, which markdown-it treats as a literal code block. Nest the include under a list item and your visitors see the form's source instead of the form. Keep it flush-left.
Match the permalink, trailing slash and all
permalink: /thanks/ writes thanks/index.html, and the canonical URL keeps the trailing slash. Point _redirect at exactly that URL — a mismatched /thanks costs an extra redirect hop on some hosts and a 404 on others.
The zero-JS path is the native path
There's no client framework waiting to intercept the submit, so the plain POST plus _redirect is a complete integration on its own. Add the fetch upgrade only when an inline status message is genuinely better than the thank-you page — it's polish, not plumbing.
From nothing to a working form.
Grab your access key
Create a form in the dashboard and copy its public key into _data/formwire.js. It's a safe-to-commit alias for your verified email.
Save the include, place it anywhere
Drop contact-form.njk into _includes and render it from a layout, a page template or a Markdown file. The markup and styling stay entirely yours.
Submissions land in your inbox
Every POST arrives as a formatted email with Reply-To set to the visitor, and as a searchable record in your dashboard.
Built for a site that ships HTML.
Every template engine, one recipe
The key lives in the data cascade, which Nunjucks, Liquid and every other 11ty engine reads the same way. Only the include syntax changes; the form doesn't.
Spam filtered server-side
The honeypot runs at the endpoint, so a script-free 11ty site keeps bots out of your inbox — and a captcha is one dashboard toggle away if a form gets targeted.
Deploys anywhere your site does
GitHub Pages, Cloudflare Pages, Netlify, a VPS with nginx — the form is static HTML, so the host only needs to serve files. Host-specific form features stay out of the picture.
Every submission stored
Beyond the email, each POST becomes a searchable record with CSV export and a JSON read API — the paper trail a static site otherwise can't keep.
Eleventy form FAQ
The questions 11ty developers actually ask.
Yes. The form is plain HTML and the key comes from the data cascade, which every 11ty template engine reads identically — {{ formwire.accessKey }} resolves the same in Liquid and Nunjucks. Only the include mechanics differ between engines; the form itself doesn't change.
Eleventy renders Markdown through Liquid by default, so a Nunjucks include tag isn't understood until you set markdownTemplateEngine: "njk" in eleventy.config.js. After that, template tags in .md files behave exactly as they do in .njk templates.
No. The browser POSTs straight to FormWire's endpoint, so the site works the same on hosts with no form features at all — GitHub Pages included — and switching hosts never touches the form.
Yes. The key is a public alias for your verified email, not a secret — delivery only ever reaches recipients you've verified. It lives in _data for tidiness, not secrecy.
Add enctype="multipart/form-data" and an <input type="file"> to the plain form, or post FormData from the fetch upgrade — the endpoint accepts both, and the attachment arrives with the notification email. File uploads are included on the Pro plan.
Start on the free tier: 250 submissions a month, unlimited forms, and the endpoint and markup are identical on every plan. Paid tiers add volume, file uploads and integrations like Slack.
See pricing →Building with another framework?
See all frameworksShip your Eleventy form today.
Grab an access key, paste an include, and route submissions to your inbox — or on to Slack, Sheets and Zapier.
Free tier forever. No credit card required.