splitforms.com
← Back to the journal

HTML Form Required Attribute Not Working? 6 Causes and Fixes (2026)

The HTML required attribute silently fails when JavaScript calls form.submit(), the button is outside the form, formnovalidate is set, or fetch bypasses…

Start free — 500 submissionsSee pricing →No card, no time limit. Pro is $5 per month.
Short answer
The required attribute fails silently when JavaScript calls form.submit() programmatically (bypasses validation), when the button is outside the <form> (doesn't trigger constraint validation), when formnovalidate is on the button, when the input has a default value (even a single space) that satisfies the check, when CSS hides the browser tooltip, or when the form is submitted via fetch/XHR. Each cause has a specific fix below. For what required does on each control when it works, see the required attribute reference.
Try it on your own form

Point any HTML form at one endpoint. 500 free submissions — no card, no time limit — and unlimited forms.

Create free account

How required is supposed to work

When you add required to a form control, the browser runs constraint validation before every native form submission. If any required field is empty (or otherwise invalid), the browser blocks the submit event, focuses the first offending field, and shows a tooltip explaining what's wrong. The user cannot proceed until they fill in the field or the validation rule is satisfied. For how the attribute behaves on checkboxes, radio groups, selects and file inputs, with a live demo, see the HTML required attribute reference.

This mechanism only works during a native form submit — that is, when the user clicks a type="submit" button that lives inside (or is associated with) the <form> element. The browser's validation pipeline is deliberately tied to this specific action. Anything that shortcuts or replaces the native submit — JavaScript methods, AJAX calls, programmatic triggers — skips validation by design. That's the root of every bug below.

<form action="/submit" method="POST">
  <input type="text"   name="name"  required />
  <input type="email"  name="email" required />
  <button type="submit">Send</button>
</form>

That form above is the gold standard. The submit button is inside the form, it has no formnovalidate, there's no JavaScript overriding the submission, and no CSS suppressing the tooltip. Every one of the six causes below breaks one of those conditions. For the broader picture — including why forms don't submit at all — see HTML form not submitting and contact form not working.

Cause 1: JavaScript submit() bypasses validation

This is the single most common reason required appears broken. Somewhere in your code — a form handler, a state management effect, a third-party widget — JavaScript calls form.submit() directly. That method submits the form without running constraint validation. Every required, pattern, minlength, and type check is skipped.

// BROKEN — skips all validation
document.querySelector("form").submit();

// In React:
const formRef = useRef<HTMLFormElement>(null);
formRef.current?.submit();

The fix is form.requestSubmit(), which was added to the DOM spec specifically for this gap. It behaves exactly like a user clicking the submit button: it runs the full validation pipeline, blocks submission if anything fails, and shows the browser's native error tooltips on invalid fields.

// FIXED — runs validation before submitting
document.querySelector("form").requestSubmit();

// In React:
formRef.current?.requestSubmit();

The API is identical — same call signature, no arguments needed — but the behavior is completely different. If validation fails, requestSubmit() returns nothing and the form stays put. If everything passes, the form submits normally. Browser support is universal as of 2026 (all modern browsers, including Safari 16.4+).

Common places this bug hides: useEffect hooks that auto-submit on mount, timer-based submissions, "save and continue" flows in multi-step forms, and third-party analytics wrappers that re-submit forms. Search your codebase for .submit() — every call site is a potential bypass. For a deeper dive into programmatic validation, see how to validate HTML forms with JavaScript.

Cause 2: Submit button is outside the <form> tag

A submit button only triggers validation for the form it belongs to. By default, "belongs to" means "is a DOM descendant of." If your submit button is rendered outside the <form> element — common in component-based layouts where the button lives in a toolbar, sticky footer, or separate component — it has no association with the form and therefore triggers no validation.

<!-- BROKEN: button is a sibling, not a child -->
<form id="contact" action="/submit" method="POST">
  <input type="text" name="name" required />
  <input type="email" name="email" required />
</form>
<div class="toolbar">
  <button type="submit">Send</button>  <!-- does nothing for the form -->
</div>

Three fixes, pick whichever fits your architecture:

  • Use the form attribute. Add form="formId" to the button. This is the cleanest fix — the button stays where it is in the DOM but explicitly associates with the form by ID.
  • Move the button inside the form. Restructure your layout so the button is a child of <form>. If a sticky footer is the constraint, make the footer a child of the form.
  • Use form.requestSubmit() from JavaScript. Wire the external button's click handler to call the form's requestSubmit() method, which triggers full validation.
<!-- Fix 1: form attribute -->
<button type="submit" form="contact">Send</button>

<!-- Fix 3: JavaScript -->
<button type="button" onClick={() => document.getElementById("contact")?.requestSubmit()}>
  Send
</button>

The form attribute is supported in all modern browsers and is the standard way to handle this in component-driven UIs. In React, this attribute works normally — no special handling needed beyond setting the form's id prop.

Cause 3: formnovalidate on the submit button

The formnovalidate attribute exists for a legitimate purpose — it lets you offer a "save as draft" or "skip" button alongside the normal submit. But when it lands on your primary submit button, either by mistake or because a template was copied incorrectly, it silently disables all constraint validation for that submission.

<!-- BROKEN: validation is completely skipped -->
<form action="/submit" method="POST">
  <input type="text" name="name" required />
  <button type="submit" formnovalidate>Send</button>
</form>

<!-- Also broken with input -->
<form action="/submit" method="POST">
  <input type="text" name="name" required />
  <input type="submit" formnovalidate value="Send" />
</form>

The fix is straightforward: remove the formnovalidate attribute from the primary submit button. Keep it only on secondary actions that genuinely should skip validation (draft saving, form reset combined with navigation, etc.). If you have both a "submit" and a "save draft" button, only the draft button should carry formnovalidate.

This attribute also exists on the <form> element itself as novalidate. If <form novalidate> is set, every submit from that form skips validation regardless of the button. Check both places if validation seems completely inert. If you set novalidate because you want custom JavaScript validation, remember to call form.checkValidity() or form.reportValidity() yourself.

Cause 4: A default value satisfies the check

The required constraint fails only when the field's value is the empty string. Any default value counts as filled: a single space, a 0, or hint text placed in value instead of placeholder. The field passes even though the user typed nothing. An empty value="" is not the problem, because it is still empty, and neither is a controlled React input initialised with useState("").

<!-- BROKEN: a space is a value, so required passes -->
<input type="text" name="name" required value=" " />

<!-- BROKEN: hint text belongs in placeholder, not value -->
<input type="text" name="company" required value="Your company" />

<!-- BROKEN: the first option has a value, so the select is never empty -->
<select name="plan" required>
  <option value="free">Choose a plan</option>
  <option value="pro">Pro</option>
</select>

The fix is to leave the default empty and put hints in placeholder. On a <select>, give the first, placeholder option value="" so required can fail until the user picks a real option. Text fields still accept a value made only of spaces, so add a pattern when blank input should not count, and trim the value on the server either way.

<!-- FIXED: no default value, hint in placeholder -->
<input type="text" name="name" required placeholder="Jane Doe" />

<!-- FIXED: the placeholder option has an empty value -->
<select name="plan" required>
  <option value="">Choose a plan</option>
  <option value="free">Free</option>
  <option value="pro">Pro</option>
</select>

<!-- Also reject input made only of spaces -->
<input type="text" name="name" required pattern=".*\S.*" />

The same applies in React, Vue and Svelte: a state initialised to "" keeps the field empty and required works, while a state initialised to a space or a label string fills the field. The HTML required attribute reference covers selects, checkboxes, radio groups and file inputs.

Cause 5: CSS hides the validation tooltip

Your form might actually be validating correctly — the submit is blocked, the field is focused — but the user sees no error feedback because your CSS is hiding the browser's native validation tooltip. Some CSS resets (normalize.css, older versions of Tailwind's preflight) include rules that suppress ::-webkit-validation-bubble or set validation-related pseudo-elements to display: none.

The result is a form that appears frozen: the user clicks submit, nothing happens, and there's no visible explanation. It's one of the most confusing failures because the underlying mechanism works — the browser is enforcing required — but the feedback channel is muted.

To diagnose, run form.reportValidity() in the console. If it returns false and focuses a field, validation is working and the problem is purely visual.

The fix is to add custom error styling using CSS pseudo-classes that work regardless of the native tooltip:

/* Highlight invalid fields after the user has interacted */
input:user-invalid {
  border-color: #ef4444;
  box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
}

/* Show a custom error message below the field */
input:user-invalid + .error-message {
  display: block;
}
.error-message {
  display: none;
  color: #ef4444;
  font-size: 13px;
  margin-top: 4px;
}

The :user-invalid pseudo-class (Baseline 2024, supported in all modern browsers) only activates after the user has interacted with the field, so you won't get jarring red borders on page load. For older browsers, fall back to the :not(:placeholder-shown):invalid chain. If you're using Tailwind, the user-invalid: variant is available in v3.4+ and v4 — see our Tailwind CSS form validation guide for the full pattern.

Important: even if you fix the CSS, the native tooltip behavior is inconsistent across browsers. Safari shows a popover, Chrome shows an inline bubble, Firefox shows a different style entirely. Custom error styling with :user-invalid gives you consistent, brandable feedback in every browser without relying on the native tooltip at all.

Cause 6: Form submitted via fetch() or XHR

If your form is submitted via fetch(), XMLHttpRequest, or any other AJAX mechanism, the browser's constraint validation is bypassed entirely. This is by design — fetch() is a generic HTTP API, not a form submission API. It doesn't know or care about your form's validation attributes.

// BROKEN — fetch() sends empty required fields
form.addEventListener("submit", (e) => {
  e.preventDefault();
  fetch("/api/submit", {
    method: "POST",
    body: new FormData(form),  // required fields can be empty
  });
});

The fix is to explicitly check validity before the fetch call. You have two options:

// Option A: checkValidity() — silent check
form.addEventListener("submit", (e) => {
  e.preventDefault();
  if (!form.checkValidity()) return;  // blocks if any field is invalid
  fetch("/api/submit", {
    method: "POST",
    body: new FormData(form),
  });
});

// Option B: reportValidity() — shows browser tooltips
form.addEventListener("submit", (e) => {
  e.preventDefault();
  if (!form.reportValidity()) return;  // shows tooltips AND blocks
  fetch("/api/submit", {
    method: "POST",
    body: new FormData(form),
  });
});

Use checkValidity() when you have custom error UI (CSS :user-invalid, inline error messages) and don't need the browser tooltip. Use reportValidity() when you want the native browser tooltips as a fallback or primary feedback. Both return true if all constraints pass and false if any fail.

This pattern applies to axios.post(), XMLHttpRequest.send(), and any other HTTP client library. The rule is the same: if you intercept the submit event with preventDefault(), you own validation from that point forward. Call checkValidity() or reportValidity() before your HTTP call.

Quick diagnostic checklist

Match your symptom to the cause, then apply the fix:

SymptomCauseFix
Form submits with empty fieldsJavaScript submit() or fetch()Use form.requestSubmit() or call form.checkValidity() before fetch()
Submit button does nothingButton is outside the <form> tagAdd form='formId' to the button or move it inside
Form always submits, no errors shownformnovalidate on the submit buttonRemove the formnovalidate attribute
Required field passes when it looks emptyA default value counts as filled, even a single spaceRemove the default value (use placeholder for hints) and trim on the server
Submit blocked but no tooltip visibleCSS hides the validation bubbleAdd custom :invalid / :user-invalid styles
AJAX call fires with empty fieldsfetch() / XHR bypasses validationCall form.checkValidity() before the fetch

If none of these match, run form.reportValidity() in the console and check the return value. If it returns false, validation is working but feedback is hidden (Cause 5). If it returns true and the form still submits with empty fields, something is stripping the required attribute at runtime — check the rendered DOM, not your source code.

Other form problems and fixes

Still stuck? Test your endpoint with our free Form Action Tester or contact splitforms support.

FAQ

Why does my form submit even with empty required fields?

Most likely cause: JavaScript is calling form.submit() or fetch() to send the data, both of which bypass the browser's constraint validation entirely. Switch to form.requestSubmit() (triggers validation before submitting) or call form.checkValidity() before your fetch() call. See Cause 1 and Cause 6 below for the code changes.

How do I check if required validation is working?

Leave a required field empty and click the submit button. If the form submits anyway, validation is broken — something is bypassing the browser's constraint check. If the form doesn't submit but you see no error message, validation is working but the feedback is being hidden (Cause 5). You can also run form.reportValidity() in the browser console — it returns true if all fields pass and false if any fail.

What's the difference between form.submit() and form.requestSubmit()?

form.submit() immediately submits the form without running any constraint validation — required, pattern, type, and minlength are all skipped. form.requestSubmit() does everything form.submit() does but runs the browser's full validation pipeline first, blocking submission and showing tooltips if anything fails. If you need to submit programmatically, requestSubmit() is almost always what you want.

Can I make required work with fetch()?

Not automatically — fetch() and XMLHttpRequest bypass the browser's constraint validation entirely because they are not native form submissions. The fix is to call form.checkValidity() (or form.reportValidity() if you want tooltips) before your fetch() call, and only proceed if it returns true. See Cause 6 for the exact pattern.

Why doesn't the browser show an error tooltip?

Two common reasons. First, your CSS (or a CSS reset like normalize.css or Tailwind's preflight) may be suppressing ::-webkit-validation-bubble or setting display: none on validation messages. Second, you may not be using native form submit — if JavaScript calls preventDefault() and then uses fetch(), the browser's tooltips never fire because the native submission never happens. Use CSS :invalid and :user-invalid pseudo-classes for custom error styling that works regardless.

Does required work with hidden inputs?

No — the HTML spec explicitly excludes hidden inputs from constraint validation. Adding required to <input type='hidden'> has no effect. The same applies to inputs with display: none, inputs inside a display: none parent, and inputs with the disabled attribute. If you need to validate a hidden value, check it in JavaScript before submitting.

Can I customize the required error message?

Yes — use the setCustomValidity() method on the input element. Call it from an oninvalid handler with your custom message string, and clear it with an empty string from an oninput handler so the field can revalidate after the user fixes it. The custom message replaces the browser's default tooltip text. For more validation techniques, see our guide on how to validate HTML forms with JavaScript.

Once validation works, you need somewhere to send the data. Get a free splitforms access key and point your form at https://splitforms.com/api/submit — submissions are emailed to you and logged in a dashboard with spam filtering built in.

Related reading

Related articles

More practical guidance from guides.

Browse the journal →
Guides

WPForms Not Sending Email? 6 Fixes That Work (2026)

WPForms notifications not arriving? Find the wp_mail cause in 2 minutes, then fix it with si…

7 min readRead →
Guides

Ninja Forms Not Sending Email? 8 Fixes for WordPress

Ninja Forms notifications not arriving? Learn why wp_mail fails, how to check email actions…

7 min readRead →
Guides

Netlify Form Not Working? 7 Fixes for Missing Submissions

Netlify form not showing submissions or returning 404? Check the 7 causes, from missing data…

7 min readRead →

Explore this topic

Start with the overview, then move into focused guides.

OverviewContact Forms for Every FrameworkStart here →GuideContact Form for Next.jsRead →GuideContact Form for ReactRead →GuideContact Form for VueRead →ReferenceContact form guideOpen →

Building forms with ChatGPT, Claude, Cursor, or v0? Connect the native MCP server and give your agent a production form backend.

Explore the MCP server →

Give your form a production backend.

One endpoint adds delivery, spam filtering, storage, and integrations. Start with 500 free submissions — no card, no time limit.

Create free accountRead the docs →
Secure checkoutSSL encryptionPrivacyProtected
VISAAMERICANEXPRESSstripe