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.
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.