Why ditch Contact Form 7 and WPForms
The default WordPress advice is "install a form plugin." That advice is fifteen years old and the reasons it made sense in 2010 don't apply anymore. Here's what a contact form plugin actually costs you in 2026:
- Performance tax. Contact Form 7 enqueues its CSS and JS on every page of your site by default — not just pages with a form. That's an extra 60–90 KB and one or two HTTP requests added to your homepage, blog posts, product pages, everything. WPForms Lite is similar. Fluent Forms is lighter but still loads conditionally.
- Security CVEs. Contact Form 7 has had multiple disclosed vulnerabilities over the years, including a critical unrestricted file upload bug in 2020 (CVE-2020-35489) that let attackers run arbitrary code. Plugins are the #1 attack vector on WordPress. Every plugin you remove is one less vector.
- Email deliverability roulette. Both Contact Form 7 and WPForms send via
wp_mail(), which uses PHP's built-inmail()by default. On shared hosting, that mail goes out from an IP with terrible reputation and lands in spam — or never arrives. That's why every form plugin tutorial ends with "now install WP Mail SMTP." Two plugins to send an email. - Lock-in. Your form data lives in
wp_cf7_submissionsor some plugin-specific table. Migrating themes, hosts, or builders means exporting and re-importing. With a hosted backend, the data lives in your dashboard regardless of what you do to WordPress. - Update treadmill. Three plugins, three update cadences, three changelogs to scan for breaking changes.
The replacement is a hosted form backend: you write plain HTML, point its action at someone else's API, and they handle email, storage, spam, and webhooks. splitforms is what this guide uses, but the technique applies to any backend.
Step 1: Get a splitforms access key (1 minute)
- Go to splitforms.com/login
- Enter your email, paste the 6-digit verification code
- Copy the auto-generated access key from the dashboard
- Set the notification email — that's where submissions land in your inbox
No credit card needed. The free tier covers 1,000 submissions per month, which is more than 95% of WordPress contact pages will ever use. If you outgrow it, Pro is $5/month for 5,000 submissions, or the $59 4-year plan averages out to $1.23/month.
Keep that access key tab open — you'll paste it into the form HTML in the next step. Also note that you can create multiple forms (one per page if you want separate inboxes and stats), each with its own key. The docs and API reference cover the request contract if you want to inspect what gets sent.
Step 2: Write the form HTML
This is the entire form. Replace YOUR_ACCESS_KEY with the key from step 1. Don't add anything else — keep it boring and it'll work everywhere:
<form action="https://splitforms.com/api/submit" method="POST" class="sf-contact">
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
<input type="hidden" name="subject" value="New contact form submission" />
<input type="hidden" name="redirect" value="https://your-site.com/thanks/" />
<label for="sf-name">Name</label>
<input id="sf-name" type="text" name="name" required autocomplete="name" />
<label for="sf-email">Email</label>
<input id="sf-email" type="email" name="email" required autocomplete="email" />
<label for="sf-message">Message</label>
<textarea id="sf-message" name="message" rows="5" required></textarea>
<!-- Honeypot: bots fill this, humans don't see it -->
<input type="checkbox" name="botcheck" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" />
<button type="submit">Send message</button>
</form>A few things to notice:
actionpoints to splitforms — that's where the browser POSTs the form data.- The hidden
access_keyis the only auth — no cookies, no nonces, no PHP session. - The
redirecthidden field sends users to a thank-you page after success. Build one with a regular WordPress page and slug it/thanks. - The
botcheckhoneypot is positioned offscreen, notdisplay:none— some bots are smart enough to skipdisplay:nonefields. - The
autocompleteattributes let mobile browsers prefill name and email, which boosts conversion noticeably on phones.
Step 3: Paste into WordPress (Gutenberg or classic)
Block editor (Gutenberg)
- Open the page where you want the form (e.g.
/contact) - Click the + button to add a block
- Search for Custom HTML and select it
- Paste the form HTML from step 2
- Click Preview in the block toolbar to confirm it renders
- Update / Publish the page
Classic editor
- Open the page in the editor
- Switch from Visual to Text tab in the top-right
- Paste the form HTML wherever you want it to appear
- Update the page — don't switch back to Visual tab before saving, or WordPress may strip attributes
If you're using a page builder (Elementor, Divi, Bricks, Beaver Builder, Oxygen), every one of them has an HTML or Code block. Drop the same snippet in there. The form is portable — same code works in a static Astro site, a Next.js app, or raw HTML at splitforms.com/free-contact-form.
Theme-friendly styling
Out of the box, the form inherits your theme's input and button styles. For most themes (Twenty Twenty-Four, GeneratePress, Kadence, Astra) that's good enough. If you want consistent styling regardless of theme, scope the CSS with the .sf-contact class we already added:
.sf-contact { max-width: 560px; margin: 0 auto; display: grid; gap: 12px; }
.sf-contact label { font-size: 14px; font-weight: 600; }
.sf-contact input[type=text],
.sf-contact input[type=email],
.sf-contact textarea {
width: 100%; padding: 10px 12px; border: 1px solid #ddd;
border-radius: 8px; font: inherit; box-sizing: border-box;
}
.sf-contact input:focus,
.sf-contact textarea:focus {
outline: none; border-color: #111; box-shadow: 0 0 0 3px rgba(0,0,0,0.08);
}
.sf-contact button {
padding: 12px 18px; background: #111; color: #fff; border: 0;
border-radius: 8px; font-weight: 700; cursor: pointer;
}
.sf-contact button:hover { background: #000; }Two places to put this CSS in WordPress:
- Customizer → Additional CSS. Easiest, theme-scoped, survives plugin updates. Appearance → Customize → Additional CSS.
- Same Custom HTML block, wrapped in
<style>tags. Self-contained, ships with the form.
Don't use !important unless your theme is fighting you — most themes don't. If yours does, scope harder with body .sf-contact.
Validation (zero JS required)
The form already validates client-side using HTML5 attributes alone. The required attribute blocks submission of empty fields. type="email" blocks malformed emails. Modern browsers show inline error messages styled by the OS — they look native because they are native.
If you want custom validation messages or async checks (like "email already submitted in last 24 hours"), add a tiny script:
<script>
document.querySelector('.sf-contact').addEventListener('submit', function (e) {
var msg = this.message.value.trim();
if (msg.length < 10) {
e.preventDefault();
alert('Please write at least 10 characters.');
}
});
</script>Splitforms also validates server-side and returns a structured JSON error if a field is missing or the access key is wrong — so you can't accidentally accept garbage even if a bot bypasses the browser checks. The response shape is documented in the API reference, but for a stock WordPress contact form you'll never need to read it — the default redirect-on-success flow handles everything.
One nuance worth knowing: by default the form does a full-page navigation to the redirect URL on success. If you want a fancier experience where the form stays on the page and shows an inline success message, swap the redirect for a small fetch() handler. We left it as a full-page redirect on purpose — it works without JavaScript, survives page builder weirdness, and degrades gracefully on browsers with strict CSP. For 90% of WordPress contact pages, that's exactly what you want.
Spam protection: honeypot vs reCAPTCHA
You have three layers of defence available:
- Honeypot (already in the snippet). A hidden field bots fill in and humans don't. Catches ~80% of spam with zero UX cost.
- splitforms AI classifier (on by default, free tier). Catches the smart bots and AI-generated spam that honeypots miss. Reads message content and scores it.
- reCAPTCHA v3 (optional). Adds a Google script and a hidden token field. Catches what slips through but adds 90+ KB and a Google dependency.
For 95% of WordPress sites, layers 1 and 2 are enough and you'll never see spam in your inbox. Only add reCAPTCHA if you're running ads or your form gets posted to spam lists. The deep comparison is in honeypot vs reCAPTCHA, and the full taxonomy of options is in this 2026 spam protection guide. Captcha-curious readers should also skim best captcha for contact form.
Test and deploy
- Open the live URL in an incognito window (so WordPress doesn't serve the editor preview)
- Fill in real-looking data — name, your email, a short message
- Submit. You should land on your
/thankspage within a second - Check the inbox attached to your splitforms form — email should arrive in under 5 seconds
- Open splitforms.com/dashboard/submissions and confirm the submission is logged
- Hit Reply in your email client — it should reply to the visitor's address, not to splitforms (because we set the form's
emailfield as reply-to automatically)
If anything breaks, the dashboard shows raw request logs — you'll see whether the request hit, what fields it had, and whether spam classification flagged it. Most "form not working" bugs are diagnosed in 30 seconds from the logs. If you want a deeper walk-through of safe test patterns, see how to test form submissions without real emails.
Performance: plugin-free vs Contact Form 7
We tested both setups on a fresh WordPress 6.7 install with the Twenty Twenty-Five theme, hosted on a Hetzner CX22 VPS with object caching enabled, measured via Lighthouse mobile profile from a US-East edge.
| Metric | Contact Form 7 | WPForms Lite | splitforms (no plugin) |
|---|---|---|---|
| JS added to /contact | ~38 KB | ~52 KB | 0 KB |
| CSS added to /contact | ~14 KB | ~28 KB | 0 KB (theme CSS only) |
| Extra HTTP requests | 2 | 3 | 0 |
| Plugin code site-wide | loaded on every page | loaded on every page | none |
| LCP (contact page) | 2.1s | 2.4s | 1.5s |
| TBT (contact page) | 180ms | 240ms | 30ms |
| Lighthouse Performance score | 78 | 72 | 96 |
The Contact Form 7 site-wide load is the bigger hidden cost — every blog post, every product page, every archive carries the plugin's CSS and JS even though only one page uses it. Removing the plugin shaves bytes off your entire site, not just the contact page.
If you're using a Core Web Vitals-scoring tool like PageSpeed Insights or CrUX, the LCP improvement is the one that compounds: faster LCP means better Search Console rankings for the contact page, and Google does roll up site-wide performance signals when evaluating overall page experience. Removing one heavy plugin can move the needle on rankings, not just on your synthetic Lighthouse score.
And then there's the operational side: no plugin to update, no premium upgrade prompt every six weeks, no "Contact Form 7 is causing a fatal error after the WordPress 6.8 update" emergency. The form is plain HTML. WordPress doesn't parse it, doesn't shortcode it, doesn't cache it differently from any other markup. It just renders.
Troubleshooting WordPress-specific gotchas
- Form HTML disappeared after saving. You switched from Text to Visual tab in the classic editor. WordPress' TinyMCE strips unfamiliar attributes. Use the Gutenberg Custom HTML block instead, or stay on the Text tab throughout.
- Page builder wraps the form in extra divs and breaks layout. Scope your CSS with
.sf-contact(we already did) and adddisplay: blockto the form if the builder forces flex. Elementor users: set the HTML widget's container width to 100%. - Cloudflare or CDN serving stale form HTML. Purge the page cache after editing. In Cloudflare: Caching → Configuration → Purge by URL. In WP Rocket: Settings → Clear Cache. If the access key just rotated, this is usually the culprit.
- Rocket Loader breaking the honeypot script. Cloudflare Rocket Loader async-loads inline JS, which can race with form rendering. Either disable Rocket Loader for the contact page (Page Rule) or add
data-cfasync="false"to any inline<script>in your form block. - Wordfence or Sucuri blocking the POST. Some security plugins flag external form POSTs as suspicious. Whitelist
splitforms.comin the firewall rules, or check the plugin's "Live Traffic" log for the blocked request. - 401 Unauthorized in browser console. Wrong access key or it has Allowed Domains set to a domain that doesn't match. Re-copy the key from the dashboard and double-check the Allowed Domains setting. This troubleshooting guide covers every status code.
- CORS error on submit. Almost always a security plugin or WAF rule. The CORS fix walkthrough covers the diagnosis flow.
- Form works in admin preview but not on live site. A caching plugin is serving a pre-edit HTML snapshot. Purge cache, then test in incognito.
Next steps
- Browse more guides: splitforms blog.
- Moving off a hosted form service? Try migrate from Formspree or the splitforms vs Formspree comparison.
- Compare alternatives: best free form backend services 2026.
- HTML form deep-dive: HTML contact form code 2026, html form action complete guide.
- Or sign up and grab an access key now: splitforms.com/login.
- Plan and security questions: FAQ.
FAQ
Will this work on WordPress.com or only self-hosted WordPress.org?
It works on self-hosted WordPress.org out of the box, and on WordPress.com Business plan and above (which allow custom HTML and external scripts). On free WordPress.com or the Personal plan, custom HTML blocks are restricted, so the form's action attribute will be stripped. If you're on WordPress.com Personal, the cheapest fix is to upgrade to Business — or migrate to self-hosted. Once HTML is allowed, the form code in this guide pastes straight into a Custom HTML block.
Does it work with Elementor, Divi, Beaver Builder, or Bricks?
Yes. Every major page builder has an HTML or Code block. In Elementor it's the HTML widget. In Divi it's the Code module. In Beaver Builder it's the HTML module. Bricks has a Code element. Paste the same form HTML from this guide and the builder will render it as-is. The only gotcha is that some builders wrap your HTML in extra divs, which can mess with CSS — scope your form styles with a unique class to avoid conflicts.
How does it compare to Contact Form 7 on page speed?
Contact Form 7 loads its CSS and JavaScript on every page of your site by default (even pages without forms), adding roughly 60–90 KB and one or two extra HTTP requests. Our plugin-free form adds zero CSS and zero JS unless you opt in to client-side validation. On a typical WordPress install we measured a 0.4–0.7 second LCP improvement on the contact page and faster TTFB site-wide because PHP isn't rendering plugin shortcodes on every request.
What about spam? Contact Form 7 has Akismet and reCAPTCHA built in.
splitforms includes AI-based spam classification on the free tier — no Akismet subscription or reCAPTCHA setup required. The honeypot field in our HTML snippet catches the dumb bots; the AI classifier catches the smart ones. If you still want reCAPTCHA, add Google's reCAPTCHA v3 script and send the token as a hidden field — read /blog/honeypot-vs-recaptcha for the trade-offs. For most small business sites, honeypot plus AI is enough.
Will the form still work if my theme uses heavy caching or Cloudflare?
Yes, because the form is plain HTML — there's no PHP rendering, no nonces, no session state. Caching plugins like WP Rocket, W3 Total Cache, and LiteSpeed Cache can safely cache the contact page. The only thing to watch: if Cloudflare's Rocket Loader is enabled, it can defer the JS on your honeypot or validation script. Either disable Rocket Loader for that page or add the data-cfasync='false' attribute to inline scripts.
Can I send the submission to multiple email addresses?
Yes. In your splitforms dashboard, open the form and add additional notification recipients (CC and BCC). You can also set a custom reply-to so when you hit Reply in your inbox it goes back to the visitor instead of to splitforms. If you need conditional routing (sales@ vs support@ based on a dropdown), add a hidden form_route field and configure routing rules in the dashboard — it's free on Pro.
Do I need to set up SMTP or an email-sending plugin like WP Mail SMTP?
No. That's the point of using a hosted form backend. splitforms handles email delivery from its own infrastructure with proper SPF, DKIM, and DMARC — your WordPress install never touches wp_mail(). You can uninstall WP Mail SMTP, Post SMTP, FluentSMTP, or whichever email plugin you were using to make Contact Form 7 reliable. One less plugin, one less attack surface, one less thing to debug at 2 AM.
What if my host blocks outbound POST requests from the browser?
This is extremely rare — almost no shared hosts do this because the request goes from the visitor's browser directly to splitforms.com, not from your WordPress server. If you're getting CORS errors, the issue is usually a security plugin (Wordfence, Sucuri) injecting headers, or a misconfigured Cloudflare WAF rule. Allow-list splitforms.com in your security plugin and re-test. /blog/cors-error-form-submission-fix has the full debug flow.