Back to blog

October 16, 2026 · NBForms Team

WordPress Contact Form Without a Plugin: What Survives the Save

Paste a plain HTML contact form into a WordPress page and reload it — for a lot of accounts, part of it is just gone. Exactly which tags survive, which don't, and why, straight from WordPress's own filtering source.

A plain HTML contact form looks like exactly the kind of thing WordPress should have no opinion about — paste it into a page, save, done. For a meaningful share of WordPress accounts, saving is exactly where it breaks: reload the page and part of the form is simply gone, with no error and no warning that anything was removed. What follows is a tag-by-tag account of what actually survives that save, verified against WordPress's own filtering source rather than guessed at from the symptom.

A stainless steel mesh strainer on a white background Photo by KAT on Unsplash

What happens to <form>

Before saving:

<form action="https://api.nbforms.com" method="POST">

After saving, for most accounts:

<!-- nothing — both the opening and closing tag are gone -->

Gone — the opening and closing tags both disappear, along with the action and method attributes they carried. Whatever sat inside the form stays on the page, just no longer wrapped in anything that would have told a browser where to send it.

What happens to <input>

Before saving:

<input type="email" name="email" required />

After saving, for most accounts:

<!-- nothing — the whole input tag is gone -->

Also gone, field by field — name, email, and message inputs all disappear the same way the form tag did. A page that had three labeled fields keeps the labels and loses every field they were labeling.

What happens to <textarea>

Before saving:

<textarea name="message"></textarea>

After saving:

<textarea name="message"></textarea>

Unchanged. This is the detail that makes the failure confusing rather than clean — a multi-line message field survives the exact same save that just deleted the form wrapped around it and the other fields next to it.

What happens to <button>

Before saving:

<button type="submit">Send</button>

After saving:

<button type="submit">Send</button>

Also unchanged. Between this and the textarea above, what's actually left on the page after a save is a visible message box and a visible button that does nothing when clicked — neither wrapped in a form nor pointed at anywhere, with no indication that two other tags used to connect them.

Why these four split the way they do

The filter responsible, wp_kses_post(), works from an explicit allow-list of tags and attributes — $allowedposttags in WordPress's own wp-includes/kses.php — and anything not on it gets removed outright rather than escaped or flagged. textarea and button are both on that list by default; form, input, select, and option are not. The file's own source carries the reason as a code comment, verbatim: "5.0.1 removed the <form> tag, allow it if a filter is allowing its sub-elements <input> or <select>." WordPress 5.0.1 is specifically where form came off the default list — and the same function still checks for exactly that condition today, quietly re-adding a narrower <form> (missing id, class, and a few other attributes a real form usually wants) only if something else has already re-added <input> or <select> first. Absent that, the four tags above split exactly as shown.

Who never sees any of this

This filter doesn't run at all for an account with the unfiltered_html capability. On an ordinary, single-site WordPress install, both the Administrator and Editor roles have it by default — content saved under either account skips the filter entirely, form tags and all. On WordPress Multisite, the bar moves: only a Super Admin has it by default, not a per-site Administrator, so the exact same account that would have been safe on a single site can still lose its form the moment that site is part of a network. Switching to the block editor's Custom HTML block doesn't change any of this either — it's still post content, filtered the same way at save time for whichever capability the logged-in account actually has.

One more way to lose it even as an admin

There's a second, separate layer worth knowing about even for an account that does have unfiltered_html: the Classic Editor's Visual mode renders content through the browser's own editable-region parsing, not a plain textarea, and reopening a page in Visual mode after it was saved can re-interpret — and sometimes mangle — markup that the server-side filter would have let through untouched. The Custom HTML block sidesteps this specific layer on purpose, by editing raw text in a plain code field rather than a rendered region, but it's a different protection from the capability check above — one is about who's allowed to save certain tags at all, the other is about whether the editor itself re-parses them first.

The fastest way to find out which case applies

Saving a page with nothing but a bare <form></form> in it and reloading the page answers the capability question directly: if the tag is still there, that account has unfiltered_html and none of the stripping above applies to it; if it's gone, everything in this post is in play. That one-tag test is faster than reasoning about roles and multisite status secondhand, and it's the same test worth repeating after a theme or hosting change, since either can change which capability an existing account actually has.

The path that skips the filter entirely

wp_kses_post() only runs against content being saved into the posts table — it has no reach into a theme's own template files, because no post content is involved in rendering one. Putting the form in a custom page template instead of a page's content editor sidesteps the entire capability question:

<?php
/* Template Name: Contact */
get_header();
?>
<form action="https://api.nbforms.com" method="POST">
  <input type="hidden" name="_token" value="YOUR_TOKEN" />
  <label>Name</label>
  <input type="text" name="name" required />
  <label>Email</label>
  <input type="email" name="email" required />
  <label>Message</label>
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>
<?php get_footer(); ?>

Saved as page-contact.php in the active theme (or a child theme, so an update to the parent theme doesn't overwrite it) and selected as that page's template in the editor sidebar, this markup reaches the browser exactly as written — regardless of which role saved it, and regardless of whether the site is a single install or part of a network.

For the same pattern expressed as plain HTML in other site builders, see the framework-specific snippets; for what happens on NBForms' side once a submission like this one arrives, see contact form without a backend.

Frequently asked questions

Why did my pasted contact form disappear after saving the page?

WordPress runs saved post and page content through a filter (wp_kses_post) for any account without the unfiltered_html capability, and that filter's default allow-list doesn't include <form>, <input>, <select>, or <option> — it quietly removes them rather than rejecting the save or showing an error.

Why are there leftover <textarea> or <button> elements after a form gets stripped?

Because those two specifically are still in the default allow-list — only <form> and the field tags around them were removed. The visible result is an orphaned textarea or a button sitting with nothing to submit, not a clean removal of the whole thing.

Which WordPress users never run into this at all?

Administrators and Editors on a normal, single-site install — both roles get the unfiltered_html capability by default, so their saved content skips this filter entirely. On WordPress Multisite, the bar is higher: only a Super Admin has it by default, not regular per-site Administrators.

Does the block editor's Custom HTML block avoid this somehow?

No — it's still post content, filtered the same way at save time for any account without unfiltered_html. The block type doesn't change which capability governs it.

What's a reliable way to add a raw form regardless of role or capability?

Put the markup in a theme file instead of post content — a custom page template, or a small template-part included from one. wp_kses_post only filters what's stored in the posts table; HTML written directly into a theme's PHP template is never passed through it, since no form content is involved in that save path at all.