How to add a contact form to WordPress without a plugin

April 5, 2026 · 7 min read

Writing desk with a laptop — adding a contact form to WordPress without a plugin

I'm going to make a statement that will annoy a small but vocal part of the WordPress community: you probably don't need a contact form plugin.

Contact Form 7 has 5 million installs. WPForms has 6 million. Gravity Forms charges you an annual license. Each of these is a perfectly fine tool, and each of them ships a lot of code that loads on every page of your site whether you need it or not. Contact Form 7 adds CSS and JavaScript globally by default. WPForms adds a larger bundle. Gravity Forms adds an even larger one. All three have had security advisories severe enough to make WordPress security newsletters in the last two years.

For a contact page that needs a name field, an email field, and a message box, this is a lot of machinery.

Here's the plugin-free alternative. It works on any WordPress site — self-hosted, managed, WordPress.com business plans, WooCommerce shops, anything — and it takes about four minutes.

The approach in one sentence

Paste a plain HTML form into a WordPress Custom HTML block (or a page template). Point its action attribute at a hosted form backend. Configure notifications in the backend's dashboard. Done.

Step 1 — Get an endpoint

Sign up for a hosted form backend. I'll use FormTo because I work on it and know the URL shape. Any of the tools I compared here will work with the same pattern.

After creating a form, you'll get a URL like:

https://api.formto.dev/f/your-form-slug

Copy it.

Step 2 — Add the form to a WordPress page

This is where you have two paths depending on your editor.

Gutenberg (block editor)

Open the page where you want the form. Click the + button to add a new block. Search for "Custom HTML." Drop the block in place and paste:

<form method="POST" action="https://api.formto.dev/f/your-form-slug" class="wp-contact-form">
  <p>
    <label for="name">Your name</label><br>
    <input type="text" id="name" name="name" required>
  </p>

  <p>
    <label for="email">Email</label><br>
    <input type="email" id="email" name="email" required>
  </p>

  <p>
    <label for="message">Message</label><br>
    <textarea id="message" name="message" rows="5" required></textarea>
  </p>

  <!-- honeypot: hidden from humans, catches bots -->
  <input type="text" name="website" tabindex="-1" autocomplete="off"
         style="position:absolute;left:-9999px" aria-hidden="true">

  <input type="hidden" name="_redirect" value="https://yoursite.com/thanks/">

  <p>
    <button type="submit">Send message</button>
  </p>
</form>

Preview the page. The form renders. Submit a test (with your own email). Publish.

Classic editor

Switch to the "Text" tab (not "Visual"). Paste the same HTML. Save.

If your theme sanitizes HTML input on the editor and strips form tags, you'll need the next approach.

Theme template (for themes that strip HTML)

Some security-hardened themes strip <form> tags from editor content. In that case, add the form directly to a page template. Create a child theme if you don't have one, then add a custom page template:

<?php
/*
Template Name: Contact
*/
get_header(); ?>

<main class="contact-page">
  <h1>Contact us</h1>
  <p>We reply within a day or two.</p>

  <form method="POST" action="https://api.formto.dev/f/your-form-slug">
    <p>
      <label>Name<br>
      <input type="text" name="name" required></label>
    </p>
    <p>
      <label>Email<br>
      <input type="email" name="email" required></label>
    </p>
    <p>
      <label>Message<br>
      <textarea name="message" rows="5" required></textarea></label>
    </p>

    <input type="text" name="website" tabindex="-1" autocomplete="off"
           style="position:absolute;left:-9999px" aria-hidden="true">

    <input type="hidden" name="_redirect" value="<?php echo esc_url(home_url('/thanks/')); ?>">

    <p><button type="submit">Send message</button></p>
  </form>
</main>

<?php get_footer(); ?>

Save as page-contact.php in your child theme. In the WordPress admin, create a new page, set its template to "Contact," publish.

Step 3 — Create a thank-you page

In the WordPress admin, create a new page titled "Thanks," with a short confirmation message:

Got it. We'll reply within a day or two.

Set its permalink to /thanks/. Publish.

Make sure the _redirect hidden input in your form matches this URL exactly, including the trailing slash.

Step 4 — Configure notifications

Back in the form backend dashboard:

  • Add your email under notifications. You'll get a verification email first.
  • Optional: paste a Slack or Discord webhook URL into the webhook field for real-time pings. Much faster than waiting for email notifications to arrive.

Submit a test from your phone. Confirm the email arrives. Confirm the thank-you page shows. You're live.

Why I prefer this over Contact Form 7 / WPForms / Gravity Forms

Plugin-based WordPress forms have three real costs that don't usually show up in the feature-comparison tables.

Page weight. Contact Form 7 loads about 30KB of JS and CSS on every page of your site by default, even pages without a form. WPForms loads more. This is not a huge number in isolation, but for sites that are already fighting Core Web Vitals scores, it's a hit you don't need.

Maintenance surface. Every plugin is another thing to update, another thing that could ship a breaking change, another thing that could introduce a security vulnerability. Contact form plugins are popular targets because they're on millions of sites. Check the WordPress security advisory database — form plugins are disproportionately represented.

Delivery quality. Plugin-based forms typically send notifications via your host's wp_mail() function, which in turn uses whatever SMTP your host provides. On shared WordPress hosting, this is notoriously unreliable. Half the form plugins also push you to install a separate SMTP plugin to fix deliverability, which is two plugins doing the work of a single hosted backend.

The hosted-endpoint approach has none of these costs. Zero plugins. Zero kilobytes of JS loaded on every page. Zero SMTP configuration. Zero plugin update nag screens.

What you give up

I want to be honest about what plugin-based forms do that this approach does not.

Drag-and-drop form builders. If you're a non-technical WordPress user who likes building forms visually, the hosted endpoint approach is slightly less friendly. You're editing HTML, not clicking around a UI. For most marketing sites this is fine; for complex multi-page forms, the visual builders still have an edge.

Conditional logic and multi-page forms. If your form needs to show or hide fields based on answers to earlier fields, or span multiple pages with progress indicators, a plugin is genuinely easier. The hosted-endpoint approach is optimized for the "short, clear contact form" case.

Payment integration in the form. If you're collecting payments alongside form fields (donations, event registrations, product orders with custom options), Gravity Forms and WPForms have deep Stripe/PayPal integrations that are a real convenience.

Pick based on what your form actually has to do. A standard name / email / message contact form is the 80% case, and for that, you do not need a plugin.

WordPress-specific gotchas to watch out for

Security plugins that strip HTML. Wordfence, Sucuri, and a few others sometimes block inline <form> tags in post content as a suspicious pattern. If your form disappears after saving, check your security plugin's logs and add an exception for the page.

Caching plugins and form submissions. If you use WP Rocket, W3 Total Cache, or similar, make sure your contact page is not serving a cached version that's missing a dynamic CSRF token. Our approach uses no CSRF tokens (the honeypot handles bot traffic instead), so caching the page is safe. Just make sure nothing is breaking the <form> HTML during caching.

Page builders with form blocks. Elementor, Divi, Beaver Builder all have their own form widgets. If you're already using one of those, you can use their widget or drop a Custom HTML block with our approach. The Custom HTML approach usually wins on page weight, because the page builder's form widget pulls in the same kind of global CSS and JS that Contact Form 7 does.

Theme styles overriding your form. Some themes aggressively style form elements in ways that break layout. Scope your form with a class (wp-contact-form) and add a tiny bit of CSS in the Customizer if needed.

The size comparison

For a rough sense of what you're saving:

Approach Plugins JS on every page CSS on every page
Contact Form 7 1 ~15KB ~12KB
WPForms Lite 1 ~40KB ~18KB
Gravity Forms 1 + paid ~60KB ~25KB
Hosted endpoint (this post) 0 0KB 0KB

Those numbers vary a bit by version and configuration. The shape is always the same: the plugins load stuff on every page of your site, and the hosted-endpoint approach loads nothing.


Create a free form on FormTo, paste the URL into your WordPress page, and you're live. No plugin, no SMTP headache, no monthly update reminders.

For the same setup on other platforms: static sites generally, GitHub Pages, Astro, Next.js, Hugo, and the rest of the framework series.

← All posts