splitforms.com
All articles/ GUIDES10 MIN READPublished June 12, 2026

HTML Form Not Submitting? 8 Markup Bugs and Fixes (2026)

HTML form not submitting? The 8 markup-level causes: missing action or method, wrong button type, nested forms, hidden required fields, disabled inputs, and preventDefault traps — with the exact fix for each.

✶ Written by
splitforms.com / blog

Founder of splitforms — the form backend API for developers. Writes about form UX, anti-spam, and shipping web apps without backend code.

Start from a known-good reference form

Before debugging, know what correct looks like. This is the minimal HTML form that submits in every browser with zero JavaScript:

<form action="https://splitforms.com/api/submit" method="POST">
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
  <input type="text"   name="name"    required />
  <input type="email"  name="email"   required />
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

Five load-bearing parts: an action URL that accepts POST, method="POST", a name on every input, a type="submit" button inside the form, and no other form wrapped around it. Every bug below breaks one of those five. (For a broader debugging flow that includes network and email failures, see why is my contact form not working — this post stays at the markup layer.)

Bug 1 & 2: missing action or method

With no action, the browser submits to the current page URL. With no method, it defaults to GET. Combine the two defaults and submitting your form just reloads the page with the data in the query string:

https://yoursite.com/contact?name=Jane&email=jane%40example.com&message=Hi

That URL appearing after submit is the diagnostic signature. It means the form "worked" in browser terms — it just GET-requested your own static page, which threw the data away. The fix is both attributes:

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

Two subtleties worth knowing. Relative actions resolve against the current URL: action="submit" on the page /contact/ posts to /contact/submit, but on /contact (no trailing slash) it posts to /submit. Use absolute URLs and this class of bug disappears. And method is case-insensitive but the value set is tiny: only get, post, and dialog are valid; anything else (like method="PUT") silently falls back to GET. The full attribute contract is in our form action complete guide and the action attribute reference.

Bug 3: the button isn't a submit button

Inside a form, a bare <button> defaults to type="submit". The problem is the other two values:

<button type="button">Send</button>  <!-- never submits -->
<button type="reset">Send</button>   <!-- clears the form (!) -->
<button>Send</button>                 <!-- submits (default) -->
<button type="submit">Send</button>   <!-- submits (explicit, preferred) -->

type="button" sneaks in two ways: copied from a UI-library example (component libraries default their Button to type="button" precisely to avoid accidental submits — MUI, Chakra, and shadcn/ui all do this when used a certain way, so check the rendered DOM), or added by a developer who wired a click handler and forgot the handler has to do the submitting now. Also check the button isn't disabled — a common React bug leaves disabled={isSubmitting} stuck true after a failed attempt.

Bug 4: the button is outside the form

A submit button only submits the form it belongs to. Belonging is determined by DOM ancestry — or by an explicit form attribute:

<!-- Broken: button is a sibling, not a child -->
<form id="contact" action="..." method="POST">...</form>
<button type="submit">Send</button>

<!-- Fixed without moving it: associate by id -->
<form id="contact" action="..." method="POST">...</form>
<button type="submit" form="contact">Send</button>

This happens constantly in component-based codebases where the button lives in a footer/toolbar component rendered outside the form component. The form="id" attribute is the standards-compliant fix and works in every modern browser.

Bug 5: nested forms (the browser deletes one)

HTML forbids a <form> inside a <form>, and the failure mode is brutal: the parser drops the inner form tag at parse time. Your source shows two forms; the DOM has one. The inner form's inputs become children of the outer form, and the inner submit button now submits the outer form — to the wrong action.

Typical scenarios: a newsletter embed pasted inside a page-wide form, a search widget inside a checkout form, or a CMS/legacy framework (ASP.NET WebForms) that wraps the entire body in <form runat="server">.

Diagnose against the parsed DOM, never your source file:

// Run in the console — should list each form with the action you expect
document.querySelectorAll("form").forEach(f => console.log(f.id || "(no id)", f.action));

// And confirm your input belongs to the form you think it does:
document.querySelector("[name=email]").form.action

The fix is structural: move one form out, or replace the inner form with a form-attribute-associated group of inputs pointing at their own form element placed elsewhere in the DOM.

Bug 6: hidden required fields block submission silently

Constraint validation runs before submission. If any control fails — empty required, pattern mismatch, bad type="email" value — the browser cancels the submit and focuses the first invalid field. But if that field is hidden (display:none, a collapsed accordion step, an inactive tab in a multi-step form), the browser can't focus it, shows nothing, and the form just doesn't submit. The only evidence is a console warning:

An invalid form control with name='phone' is not focusable.

Fixes, pick one: remove required from fields hidden at submit time; toggle the disabled attribute on hidden steps (disabled controls skip validation and submission — see bug 7); or validate in JS and set novalidate on the form. To quickly test whether validation is what's blocking you, run form.reportValidity() in the console — it returns false and highlights the offender.

Bug 7: disabled fields vanish from the payload

Per spec, disabled controls are excluded from form serialization. A form that submits fine but arrives missing fields often has this bug — someone used disabled to make a pre-filled field uneditable:

<input name="plan" value="pro" disabled />   <!-- sends nothing -->
<input name="plan" value="pro" readonly />   <!-- sends plan=pro -->

readonly keeps the value in the submission. Caveat: readonly doesn't exist for <select>, checkboxes, or radios — for those, keep the control enabled and enforce server-side, or duplicate the value into a hidden input. While you're here, remember unchecked checkboxes also send nothing at all (not false — literally absent), which surprises every backend developer once.

Bug 8: preventDefault without a follow-through

The final markup-adjacent bug: a script intercepts the submit event, cancels the native submission, and then fails to send the data itself.

form.addEventListener("submit", (e) => {
  e.preventDefault();          // native submit cancelled
  if (!validate()) return;     // validate() throws or always returns false
  send(form);                  // never reached
});

Variants of the same trap: a legacy onsubmit="return validate()" where validate returns undefined (falsy → cancelled); a third-party script (analytics wrapper, anti-bot widget) that hooks submit globally; or jQuery's e.stopImmediatePropagation() preventing your handler from ever running. To find hidden handlers: Elements panel → select the <form>Event Listeners tab → expand submit. Temporarily test the raw markup by running form.submit() in the console — .submit() bypasses both submit handlers and validation, so if it works, your markup is fine and the bug is in a script.

Once it submits: where does the data go?

Fixing the markup gets the request out of the browser — then it needs a server that accepts POST. If you're on a static host, there isn't one (that's the 405 error). The no-backend answer is a form endpoint: splitforms accepts standard HTML form posts at https://splitforms.com/api/submit, emails you each submission, filters spam, and logs everything in a dashboard. Free for 500 submissions/month — start from the free contact form template or generate markup with the HTML form generator, then verify end-to-end with the pre-launch test checklist.

FAQ

What happens if an HTML form has no action attribute?

The browser submits to the current page's URL. That's valid HTML — the form does submit — but unless the current URL has a server handler that accepts POST, you get the page reloading with nothing happening, or a 405 error. If you see your own page reload with form values in the query string, your form is also missing method="POST" and defaulted to GET. Always set both: action pointing at a real endpoint and method="POST".

Why doesn't my submit button submit the form?

Check three things in order. First, the button's type: <button type="button"> never submits — it must be type="submit" or have no type attribute while inside the form. Second, position: the button must be inside the <form> element (or reference it via form="form-id"). Third, JavaScript: a click or submit handler calling preventDefault() (or returning false in legacy onsubmit code) cancels the native submission. Inspect the button in DevTools and check the Event Listeners panel for handlers you didn't expect.

Can you nest one HTML form inside another?

No. Nested forms are invalid HTML, and the browser's parser silently drops the inner <form> tag at parse time. The remaining inputs get adopted by the outer form or orphaned entirely, so submit buttons stop working in confusing ways. This bites people who paste a form widget (search box, newsletter embed) inside an existing form, or whose CMS wraps the whole page body in a form (classic WebForms). Check with document.querySelectorAll('form') and inspect the parsed DOM — not your source — in the Elements panel.

Why are disabled input values not submitted?

By spec, disabled controls are excluded from form serialization — their name=value pairs never enter the request body. If you disable fields to make them read-only, the server receives nothing for them. Use the readonly attribute instead, which keeps the field non-editable but still submits its value. Note readonly doesn't apply to <select> or checkboxes; for those, keep them enabled and enforce on the server, or mirror the value in a hidden input.

Why does pressing Enter not submit my form?

Implicit submission (Enter in a text field) requires the form to have a submit button, or to contain only a single text-type input. If your form has multiple text fields and no <button type="submit"> or <input type="submit">, Enter does nothing in most browsers. Also, if the first submit-type element in DOM order is a button you've wired to something else, Enter triggers that one. Add a real submit button — visually hidden if necessary — and Enter works.

Do I need JavaScript to submit an HTML form?

No. A plain <form action="..." method="POST"> with named inputs and a submit button is a complete, working submission mechanism — browsers have shipped it since the 1990s. JavaScript is only needed if you want to stay on the page (AJAX), do custom validation, or transform data before sending. If your no-JS form doesn't submit, the bug is in the markup, which is exactly what this post catalogs.

Where do I point my form's action if I don't have a server?

Use a hosted form endpoint. splitforms gives you a URL that accepts standard HTML form posts: set action="https://splitforms.com/api/submit" method="POST", include a hidden access_key input from your dashboard, and name every field. Submissions are emailed to you and logged in a dashboard with spam filtering built in. The free plan covers 500 submissions per month, which is more than most contact forms see in a year.

Need an endpoint that actually accepts POST? Get a free splitforms access key and point your form at https://splitforms.com/api/submit.

Related: full contact form debugging guide, the form resubmission popup, complete HTML contact form code, and the splitforms docs.

About the author
✻ ✻ ✻

Get your free contact form API key in 60 seconds.

500 free form submissions per month. No credit card. No SDK, no PHP, no plugin. Drop one POST endpoint in your form and submissions land in your dashboard. Starter adds inbox delivery.

Generate access key →Read the docs
founders pricing locked in · early access open