Why Webflow's built-in form isn't enough
Most people building a Webflow site assume the native Form Block just works. It doesn't — at least not for free, and not at any meaningful volume. Here's the actual breakdown as of 2026-05:
- Starter plan (free): 50 form submissions per month, total, across the whole site. After 50, Webflow drops them silently.
- Basic plan ($14/month): 500 submissions/month and email forwarding is unreliable — many users report submissions sitting in the dashboard without ever reaching their inbox.
- CMS plan ($23/month): 1,000 submissions and email forwarding works. This is the plan Webflow funnels you toward for a working contact form.
- Business plan ($39/month): 2,500 submissions, file uploads, and reCAPTCHA Enterprise.
The $23/month CMS plan is a $276/year tax just to have a contact form that emails you. Webflow markets it as a CMS upgrade, but for plenty of small sites the only reason to upgrade is forms — you don't even need the CMS part. Founders building a portfolio, a single-page landing page, or a small marketing site routinely upgrade to CMS for one reason: their contact form stopped working at submission #51.
There's a cleaner path: replace Webflow's Form Block with a standard HTML <form> inside an Embed element, point its action at a free form-to-email backend, and skip the plan upgrade entirely. The form still looks like Webflow because you bind the same classes. The form still validates because HTML5 has that built in. And submissions go straight to your inbox.
This approach also unlocks features Webflow's built-in form doesn't support at any plan tier: signed webhooks, AI spam filtering, auto-responders, and CSV export. Free, while Webflow charges $23–$39/month for a worse experience.
Step 1: Get a splitforms access key (1 minute)
splitforms is the form-to-email backend we'll point Webflow at. Free tier is 1,000 submissions/month — 20x what Webflow's Starter plan allows — and includes AI spam filtering, webhooks, and email forwarding out of the box.
- Go to splitforms.com/login
- Enter your email, paste the 6-digit code
- The dashboard auto-generates an access key — copy it
- Set the notification email (where submissions get forwarded) in Settings → Notifications
No credit card. No plan to pick. The key looks like sf_live_a1b2c3d4... — keep it handy for the next step. If you're evaluating other backends first, the best free form backend comparison covers the alternatives, but splitforms has the highest free tier of any in that list.
One detail worth pointing out: the access key is a public identifier, not a secret. It will live inside your Webflow page's HTML where anyone viewing source can see it. That's fine — splitforms protects against abuse with rate limiting, the Allowed Domains setting in the security tab, and the AI spam classifier. If you ever suspect a key is being abused (a sudden flood of garbage submissions from a single IP), rotate it from the dashboard with one click, paste the new value into your Embed element, and republish. The whole rotation takes 30 seconds.
Step 2: Build the HTML form
Below is the exact HTML to paste into Webflow's Embed element. Swap YOUR_ACCESS_KEY for the one from your dashboard. Don't change the action URL; that's splitforms' submission endpoint.
<form action="https://splitforms.com/api/submit" method="POST" class="wf-form">
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
<input type="hidden" name="redirect" value="https://yoursite.com/thanks" />
<input type="hidden" name="subject" value="New contact from yoursite.com" />
<label class="wf-label" for="name">Name</label>
<input class="wf-input" type="text" id="name" name="name" required />
<label class="wf-label" for="email">Email</label>
<input class="wf-input" type="email" id="email" name="email" required />
<label class="wf-label" for="message">Message</label>
<textarea class="wf-input wf-textarea" id="message" name="message" rows="5" required></textarea>
<!-- Honeypot: hidden from humans, bots check it -->
<input type="checkbox" name="botcheck" style="display:none" tabindex="-1" autocomplete="off" />
<button class="wf-button" type="submit">Send message</button>
</form>Three things to notice in this template:
- Hidden
access_key— authenticates the request. Without it, splitforms returns 401. - Hidden
redirect— where the visitor lands after a successful submit. Drop a real URL to a Webflow page like/thanks. - Hidden
botcheckcheckbox — the honeypot. Real users never see it; bots that fill every field get blocked. See honeypot vs reCAPTCHA for why this beats puzzle-CAPTCHAs.
Class names wf-form, wf-input, wf-button are placeholders — replace them with whatever class names you already use in your Webflow design system. The next section walks through that binding.
One subtle point: name attributes become the keys in the email and dashboard. Use friendly field names like customer_name, company_name, budget_range. splitforms preserves whatever you set — no required schema. The same form HTML can ship as a quote-request, newsletter signup, or RSVP just by changing field names.
To capture which Webflow page the visitor was on, add <input type="hidden" name="source_page" value="" /> plus a one-liner: document.querySelector('[name=source_page]').value = location.pathname;. The submission email will tell you exactly which landing page generated the lead.
Step 3: Embed the form in Webflow
Open your Webflow project in the Designer and navigate to the page where the form should live (Contact, Home, wherever).
- In the Add panel (left sidebar, press A), search for Embed.
- Drag the Embed element onto your page where you want the form.
- A code editor opens. Paste the HTML from Step 2.
- Click Save & Close.
- The Designer preview shows a placeholder rectangle — that's normal. Embed code doesn't execute in the Designer; you'll only see the real form on the published site.
One important Webflow gotcha: the Embed element accepts up to 10,000 characters per instance. A standard contact form is around 1,200 characters, so you have plenty of room. If you're embedding a much larger snippet, split it across multiple Embed elements or move the bulk into a site-wide custom code block under Site Settings → Custom Code.
To publish a preview, hit Publish in the top right and choose your .webflow.io staging subdomain. Live URL is something like yoursite.webflow.io/contact.
If you're working in a Webflow team workspace, the Embed element requires the "Edit code" permission. Most clients on lower-tier workspace plans don't have it by default — if you're a freelancer building this for a client, you may need to ship the form yourself or have the workspace owner promote your role. The published output is identical either way; the restriction is purely an editor-side permission.
One nice consequence: you can swap the form's HTML in 10 seconds without touching the Designer's visual tree. Client wants a quote request instead of a generic contact form? Open the Embed, replace inputs, save, republish.
Step 4: Bind Webflow design-system classes
This is the step most tutorials skip and it's why their forms look like a 2003 PHP script bolted onto a polished Webflow site. The fix is straightforward: use the class names Webflow already generated for your style guide.
In the Webflow Designer, click any existing styled element (a button on your homepage, an input on a different page) and check the Selector dropdown at the top of the Style panel. You'll see class names like Button Primary, Form Field, Heading Style 3. Webflow stores those as kebab-case class names — button-primary, form-field, heading-style-3.
Take those class names and swap them into the HTML you pasted:
<!-- Before: placeholder classes -->
<input class="wf-input" type="text" name="name" required />
<button class="wf-button" type="submit">Send</button>
<!-- After: Webflow's actual classes from your style guide -->
<input class="form-field w-input" type="text" name="name" required />
<button class="button-primary w-button" type="submit">Send</button>The w-input and w-button classes are Webflow defaults that get injected automatically — they cover base resets like font inheritance and box-sizing. Your custom class (form-field, button-primary) layers your design on top. If you don't know your custom class names, inspect the published page in Chrome DevTools and copy them out of the rendered DOM.
You can also wrap the form in a <div class="container"> using whatever container class your site uses for content width. After this binding step, the form is visually indistinguishable from a native Webflow Form Block — except it actually delivers email.
A more elegant alternative if you have a lot of forms across the site: define a single .form-block class in your Webflow style guide and style every form descendant in CSS rather than per-element. Drop a <style> block in Site Settings → Custom Code → Head Code with rules like .form-block input, .form-block textarea {...}. Then any Embed-element form wrapped in <div class="form-block"> inherits the styling automatically. This is how production Webflow sites with multiple contact, signup, and quote-request forms stay consistent without re-binding classes 50 times.
Step 5: Form validation and UX polish
HTML5 validation handles the basics for free — the required attribute on each input blocks submission if empty, and type="email" enforces a valid email format. The browser shows a native error tooltip without any JavaScript.
If you want custom validation messages, add a small inline script:
<script>
document.querySelectorAll('.wf-form input, .wf-form textarea').forEach(el => {
el.addEventListener('invalid', (e) => {
e.preventDefault();
el.classList.add('is-invalid');
el.setCustomValidity(
el.name === 'email'
? 'Please enter a valid email address.'
: 'This field is required.'
);
});
el.addEventListener('input', () => {
el.classList.remove('is-invalid');
el.setCustomValidity('');
});
});
</script>Paste that into the same Embed element below the form, or into Site Settings → Custom Code → Footer Code if you want it site-wide. The .is-invalid class lets you style failed inputs in Webflow (red border, etc.) — define it in your style guide once and it works on every form. Framework users porting this approach to a React stack should check the React form backend guide; the validation pattern is identical.
If you want a loading state on the submit button while the request is in flight, intercept the submit event and post via fetch instead of letting the browser do a full page navigation. That gives you an AJAX UX: spinner on the button, success message inline, no redirect needed. The trade-off is more JavaScript and slightly worse fallback if a visitor has JS disabled — for a standard contact form, the plain-form redirect approach in Step 2 is honestly cleaner and more reliable. Pick AJAX only if you have a strong design reason for an inline success state.
Step 6: Spam protection (no reCAPTCHA needed)
The botcheck honeypot input from Step 2 catches roughly 95% of dumb spambots — they fill every form field they see, including hidden ones, and splitforms drops the submission silently when that field is non-empty.
splitforms also runs an AI classifier on every accepted submission and flags suspicious ones in the dashboard without bouncing them. That covers the trickier bots that skip honeypots. Combined, you're looking at <1% spam reaching your inbox without showing visitors a single CAPTCHA puzzle.
If you want belt-and-suspenders, add Cloudflare Turnstile (free, no images to identify) by including their script in Site Settings → Custom Code → Footer Code. But for a standard contact form, the honeypot + AI classifier combo is enough. Deeper read: honeypot vs reCAPTCHA — which actually works.
One thing most Webflow tutorials get wrong: they tell you to enable Webflow's built-in reCAPTCHA in Site Settings → Forms. That only protects native Webflow Form Blocks. Embed-element forms are invisible to that setting and will not be protected by it. Don't waste time toggling Webflow's reCAPTCHA hoping it'll guard the splitforms form — it won't. The honeypot is the actual protection layer on Embed-element submissions, and splitforms' server-side classification is the second line of defense.
For high-volume sites attracting targeted scrapers (job boards, lead-gen pages with valuable email lists), splitforms also offers per-IP rate limits and country blocking in the dashboard. Most contact forms will never need either, but they're there. The free tier includes both.
Step 7: Test in preview and deploy
Webflow's Designer preview (the eye icon) does not execute Embed code. You'll see a placeholder where the form should be. That's a Webflow quirk, not a bug in this setup. To actually test, publish to staging:
- Click Publish (top right of Designer)
- Choose Publish to selected domains
- Check the
yoursite.webflow.iostaging checkbox (leave the custom domain unchecked for now) - Click Publish to Selected Domains
- Open
yoursite.webflow.io/contactin a new tab - Fill out the form and submit
- Within 5 seconds: notification email arrives + submission visible in splitforms.com/dashboard/submissions
- If both checks pass, re-publish with your custom domain checkbox enabled to push live
The 5-second number is real — splitforms' submission API typically responds in <200ms and the SMTP send pipeline adds a couple seconds. If you wait longer than 30 seconds for the email, skip to the troubleshooting section.
A practical staging-versus-production tip: keep the redirect hidden input pointed at a real /thanks page that exists on your Webflow site. If the URL 404s after a successful submission, the visitor sees the splitforms default JSON success page — which is functionally fine but looks unbranded. Build a one-section Webflow page at /thanks with a heading like "Got it — we'll reply within one business day" and a button back to the homepage. Took me 4 minutes the first time I shipped this; my conversion-page bounce rate dropped because visitors felt like the form actually went somewhere.
Also test from a mobile device. Webflow forms on mobile sometimes show keyboard-related layout shifts that don't appear on desktop preview. The Embed approach inherits all the same iOS / Android keyboard behavior as a native HTML form (since that's exactly what it is). If a field is hard to tap, add autocomplete attributes (autocomplete="email", autocomplete="name") so mobile browsers can offer one-tap autofill.
Troubleshooting common Webflow gotchas
- Form submits but no email arrives. Check splitforms dashboard first — if the submission is there, the issue is email forwarding (check spam folder, verify the notification email address in Settings). If it's not in the dashboard at all, the form never reached splitforms — see CORS below.
- splitforms returns 401 Unauthorized. Wrong access key, typo, or leading/trailing whitespace. Re-copy from the dashboard. If you set Allowed Domains in splitforms' security tab, make sure both your
.webflow.ioURL and your custom domain are in the allow-list. - CSP / CORS errors in the browser console. Webflow lets you set a Content-Security-Policy header in Site Settings → Advanced → Custom Code. If you've added a strict CSP, allow
https://splitforms.comin theform-actiondirective:form-action 'self' https://splitforms.com;. By default Webflow does not set CSP, so most users skip this. - Custom code limit hit on free Webflow. Site-wide custom code (in Site Settings) is capped at 10,000 characters on the Starter plan. The Embed element has its own per-instance 10,000-char budget — they don't share. If you've added other custom code site-wide and are bumping the cap, move the inline validation script into the Embed element itself.
- Form works on .webflow.io but not on the custom domain. You re-published to staging but forgot to re-publish to the custom domain. Click Publish, check both domain boxes, republish.
- Webflow's default reCAPTCHA error overlay appears. You accidentally left the native Form Block in place. Delete it — the Embed element is the only form you should have on the page.
- Designer preview shows raw HTML instead of the form. That's expected. The Designer doesn't execute Embed code. Test on the published
.webflow.ioURL instead.
If the form still won't cooperate after working through this list, the deliverability checklist in contact form not working — the deliverability checklist covers SPF/DKIM and inbox-side issues that are usually the culprit. Reference docs: /docs, /api-reference.
What to do next
- Side-by-side cost breakdown vs Webflow's built-in form: splitforms vs Formspree and splitforms vs Web3Forms.
- Grab a pre-wired template: free HTML contact form — paste it straight into Webflow's Embed.
- Building the same flow on a different platform? Browse all tutorials.
- Migrating an existing form from a paid service: migrate from Formspree to splitforms in 5 minutes.
- Plan, security, and EU residency questions: /faq. Ready to wire your form: get a free access key.
FAQ
Why doesn't Webflow's built-in form work without an upgrade?
Webflow's native form element technically renders on every plan, but submissions are capped at 50/month on the free Starter plan and 500/month on the Basic plan. To get past those caps you have to upgrade to the CMS plan at $23/month or the Business plan at $39/month. Worse, on the free plan, submissions don't even reach your inbox reliably — they sit in the Webflow dashboard and need a paid plan to forward by email. The Embed-element workaround in this guide bypasses the cap entirely because submissions never touch Webflow's backend.
Will this break Webflow's drag-and-drop styling?
No. The Embed block is just a container for raw HTML. You bind the same CSS classes Webflow generated for your design system (look in the Style panel for class names like `w-input`, `w-button`, or any custom class you defined) and the form picks up your site's typography, spacing, and colors automatically. If your form doesn't visually match the rest of the site, you forgot to apply your global class names to the inputs inside the embed.
Does this count against Webflow's custom code character limit?
The Starter plan limits site-wide custom code (in Site Settings) to 10,000 characters, but Embed elements don't share that budget — they have their own per-element limit of 10,000 characters, which is way more than a contact form needs (a full form is usually under 1,500 characters of HTML). You can have multiple Embed elements on multiple pages without hitting any plan-wide cap. The constraint only matters if you're embedding very large widgets.
Can I use this on a free Webflow site (.webflow.io subdomain)?
Yes. The Embed element works on every plan including the free Starter, and a splitforms form has no domain restriction by default. The only gotcha is that submissions from a `.webflow.io` staging subdomain look different in your dashboard than submissions from your custom production domain — if you set Allowed Domains in splitforms' security tab, add both your `.webflow.io` URL and your custom domain to avoid surprise 401s during staging tests.
What about file uploads — does Webflow's Embed support them?
Webflow's native form element doesn't support file uploads at all (that's a separate $23/month CMS feature). The splitforms approach supports files via standard `<input type='file' name='attachment' />` and `enctype='multipart/form-data'` on the form. Files up to 10 MB are accepted on the free plan and attached to the notification email. You don't need a Webflow plan upgrade — the upload happens client-side directly to splitforms.
How do I redirect to a thank-you page after submission?
Add a hidden input named `redirect` with the destination URL as the value: `<input type='hidden' name='redirect' value='https://yoursite.com/thanks' />`. splitforms will 302-redirect the browser to that URL after a successful submission. If you skip the redirect, the visitor sees a default JSON success page — which is fine for AJAX flows but ugly for plain form posts. Always set a redirect for a clean visitor experience.
Will Webflow's reCAPTCHA still work with this method?
Webflow's built-in reCAPTCHA only works with Webflow's native form element — it won't fire on an Embed-element form. That's actually fine: splitforms includes AI spam classification on the free tier (see /blog/honeypot-vs-recaptcha for the benchmark), so you don't need reCAPTCHA anyway. Add the honeypot input shown in this tutorial and you'll catch 99% of bots without showing your visitors a puzzle to solve.
Can I test the form inside Webflow's Designer preview?
Webflow's Designer preview (the eye icon) doesn't execute custom code — Embed elements show a placeholder. To actually test the form, publish to your `.webflow.io` staging subdomain and submit from there. Submissions arrive in your splitforms dashboard and email inbox within ~5 seconds. Don't bother trying to test inside the Designer; that's a Webflow limitation, not a splitforms one.