Contact form for Eleventy (11ty) websites
11ty's beauty is its simplicity — and a contact form should match. One Nunjucks (or Liquid) include, one form tag, no serverless function in sight. Pull the access key from global data, render zero JavaScript, and ship submissions straight to your inbox.
What your Eleventy contact form actually looks like.
Drop-in form backend with spam filtering, signed webhooks, and a real submissions dashboard. The same code in this preview is what you copy into your Eleventy project — no SDK, no plugin, no PHP.
- ✓1,000 submissions per month, free forever
- ✓Honeypot + AI spam classifier on every plan
- ✓Signed webhooks to Slack, Discord, your server
Ship a Eleventy contact form without a backend.
No SDK, no PHP, no plugin. Your form posts standard FormData to one URL — submissions land in your inbox.
Get your free access key
Verify your email and your access key is generated instantly. Free for 1,000 submissions per month, forever.
By signing up, you agree to our terms and privacy policy.
Drop in the Eleventy code
Copy the Eleventy snippet on the right and paste it into your project. Replace YOUR_ACCESS_KEY with the key from step 1.
Submissions land in your inbox
Hits your dashboard and email in seconds. Forward to Slack, Discord, Sheets, Notion, or any signed webhook URL.
Try it now — no signup, no key.
This is a styled HTML preview of what your Eleventy form will look like. Submitting opens a confirmation, no real request is sent.
Your Eleventy form posts FormData to /api/submit. Splitforms validates the access key, runs the spam classifier, and forwards the parsed submission to your inbox plus the dashboard.
- →14ms median round-trip from the edge.
- →Honeypot + classifier, no CAPTCHA.
- →Per-domain key locking out of the box.
{
"access_key": "sk_live_4f9a_••••",
"name": "Maya Iyer",
"email": "maya@studio71.co",
"message": "…"
}How to ship this without regrets.
Five rules that make the difference between a form that works in the demo and a form that survives launch traffic.
- 01
Define the key in `_data/site.js` (not site.json), reading from `process.env.SPLITFORMS_KEY`. Add a `.env` file to gitignore. Local dev and production both read from environment.
- 02
Use a Nunjucks include (`{% include "partials/contact-form.njk" %}`) so the form is reusable across templates without copy-paste.
- 03
Build the /thanks page as a normal Eleventy content file (`content/thanks.md`) so it inherits your layout, header, and footer.
- 04
Lock the access key to your production domain in the splitforms dashboard — Eleventy's local dev server (port 8080) needs a separate dev key or be added to allowed domains.
- 05
If your site uses 11ty Image or 11ty Bundle plugins, the contact form template still works as plain HTML — no plugin compatibility issues.
What bites people who skip the docs.
Worth a 60-second skim before you ship to production. Each one has caused a Eleventy support ticket at least once.
Eleventy v3 is ESM-only — your config must be .mjs
If you upgraded to Eleventy 3 and your .eleventy.js (CommonJS) silently stopped exposing globals, that's why. Rename to eleventy.config.mjs and use ESM export default. Otherwise addGlobalData('splitformsKey', …) won't reach your templates.
Liquid filters and Nunjucks filters have different names
{{ '/thanks/' | url }} works in Nunjucks (with the eleventy-plugin-url plugin). In Liquid, the filter is | url_for or you skip the filter entirely and write the path literally. Mismatched filter names render nothing — the form's redirect URL becomes empty.
Global data with sensitive values gets committed by accident
If you put your access key in _data/site.json (the obvious place), it ships to your repo. Use _data/site.js and read from process.env.SPLITFORMS_KEY instead — then add .env to gitignore. Eleventy auto-loads .env if you have dotenv installed.
Permalinks: false on the contact page makes the redirect fail
If your contact page has permalink: false (rare but possible), it's not built — and splitforms's redirect target points at a 404. Always ensure both the form page and the /thanks page have valid permalinks.
Pagination data clobbers your `splitformsKey` global
If you reuse the variable name splitformsKey inside a paginated template's data, Eleventy's data cascade has paginated data win over global data. Use a unique-enough name like siteSplitformsKey to avoid the conflict.
Eleventy serverless (`eleventy-plugin-serverless`) bundles env vars at build, not request time
If you use eleventy-plugin-serverless to render the contact page on-demand via Netlify Functions, you might assume process.env.SPLITFORMS_KEY is read per-request. It isn't — Eleventy serverless bundles the rendered template into a Netlify Function during build, capturing the env vars from your build environment. Rotating the key in Netlify's dashboard does nothing until the next deploy. Fix: read the key inside the form template via <%= process.env.SPLITFORMS_KEY %> rendered at request time using a Nunjucks {% raw %} escape, or skip serverless rendering for the contact page.
How Eleventy handles forms without splitforms.
The shape of the problem before splitforms enters the picture — and the gap it fills for Eleventy specifically.
Eleventy is a Node-based static site generator — it produces HTML at build time and ships nothing else. There is no runtime, no /api/contact endpoint, no hooks for handling a form POST. The historical workarounds: (a) deploy to Netlify and use Netlify Forms, (b) write a Cloudflare Worker that handles POST /contact and forwards to your email provider (~4 hours plus ongoing operation), or (c) use a third-party form API (Formspree, Web3Forms, Basin, splitforms). Eleventy's data cascade lets you neatly pull an access key from _data/site.js and use it in a Nunjucks/Liquid include — but the actual delivery layer is always external. Splitforms is the lowest-friction external option.
Two ways to ship splitforms on Eleventy.
Pick the pattern that matches your constraints — JS budget, key-exposure tolerance, server-side opacity. Both produce the same result.
Pattern A — Nunjucks include from `_includes/partials/`
Save as src/_includes/partials/contact-form.njk. Use from any template with {% include "partials/contact-form.njk" %}. Pulls the key from a global data file that reads process.env.SPLITFORMS_KEY.
Pattern B — Eleventy v3 ESM config
Eleventy 3 is ESM-only. Use eleventy.config.mjs and addGlobalData to wire the key from environment. Liquid templates work the same way — just save as .liquid.
Shipping Eleventy + splitforms to production.
Host-specific gotchas, env-var conventions, and the boring-but-load-bearing details for putting this on the public internet.
Eleventy deploys to any static host. The form posts cross-origin to splitforms.com so the host is irrelevant for delivery. Eleventy v3's ESM-only config is the major upgrade gotcha — .eleventy.js (CommonJS) silently stops exposing globals; rename to eleventy.config.mjs. Local dev runs on localhost:8080 — add to splitforms allowed-domains for testing or use a separate dev key. _data/site.js (not .json) lets you read from process.env; keep .env in gitignore. For 11ty + Netlify (the classic combo), you can use Netlify Forms instead, but you cap out at 100/mo — splitforms gives 1,000/mo and works on every host.
splitforms vs native eleventy.
What you get for free vs what you build, pay for, or do without.
Things developers ask before they integrate.
Direct answers, no marketing fluff. Missing one? Email hello@splitforms.com.
Ship your Eleventy contact form in 60 seconds.
1,000 free submissions per month. No credit card. Lock the access key to your domains, paste the snippet, watch submissions land in your inbox.
