JavaScript Form Validation Table

PieceWhat it doesField note
field.validity.validThe master flagfalse means ANY of the constraint states fired - check the individual states for why
validity.valueMissing / typeMismatch / patternMismatch / tooShort / rangeUnderflow / stepMismatchWHY it failedThe states map 1:1 to HTML attributes: required / type / pattern / minlength / min / step - no guessing
field.checkValidity()The silent checkReturns a boolean, paints nothing - fires the invalid event on the field when false
form.reportValidity()The native UI passPaints the browser error bubble on the FIRST invalid field and focuses it - free UX most sites disable
field.setCustomValidity('msg')Your own ruleAny non-empty string marks the field invalid; the empty string clears it - the escape hatch for cross-field rules like password confirmation
:invalid vs :user-invalidStyling the state:user-invalid waits until the user typed or left the field - no red-on-load wall of shame on empty required inputs
novalidate on the formTurns OFF native blockingThe submit event always fires and YOU own the error UI - without it, an invalid form just refuses to submit with zero events
the invalid eventThe one hookFires per-field on a failed checkValidity() - the hook for rendering your own error text; it does NOT bubble
Reference: the MDN Constraint Validation API guide. The browser ships a complete validation engine: HTML attributes (required, type=email, pattern, min, maxlength) define the rules, validity carries the per-field verdict broken into named states, and two methods read it - checkValidity() to ask silently, reportValidity() to show the native bubbles. The dirty secret of most hand-rolled validation code is that it rebuilds this badly: the native engine already gives per-rule error classification for free.
The golden pair most tutorials skip: setCustomValidity() is how you inject cross-field or server-verified rules into the same engine (passwords match, username free) - and you MUST clear it with setCustomValidity('') when the user fixes the input, or the field stays stuck invalid forever. Style with :user-invalid, not :invalid, so users see errors only after touching a field. And understand novalidate before fighting it: it hands you the submit event and full UI ownership - it does not disable validation, it just stops the browser from painting its own bubbles.
Related tools: the CSS pseudo-classes table (where :valid/:invalid/:user-invalid live), the FormData table (the serialized payload once validation passes), the event listeners table (the submit and invalid wiring), and the dialog element table (the modal that hosts the form and its showModal focus rules).

The browser ships a complete validation engine and most hand-rolled validation code rebuilds it badly: HTML attributes (required, type=email, pattern, minlength, min, step) define the rules, field.validity carries the per-field verdict broken into named states, and two methods read it - checkValidity() asks silently, reportValidity() paints the native error bubble on the first invalid field and focuses it.

Bottom line: the golden pair most tutorials skip is setCustomValidity - any non-empty string marks a field invalid (your cross-field rules like password confirmation join the same engine), and you MUST clear it with setCustomValidity('') when the user fixes the input, or the field stays stuck invalid forever.

The honest part: the native bubbles are the browser look and you cannot restyle them. Projects that need fully custom error UI take the novalidate route - which does NOT disable validation, it hands you the submit event and full UI ownership - and then every error message is yours to render from the validity states, per field, on the invalid event.

How to use

  1. Define rules in HTML first: required, type=email, pattern, minlength, min - each attribute maps 1:1 to a named state in field.validity (valueMissing, typeMismatch, patternMismatch, tooShort, rangeUnderflow), so the WHY is free.
  2. Ask silently: if (field.checkValidity()) - a boolean, paints nothing, fires the invalid event on the field when false. Use it for inline checks as the user types.
  3. Show the native UI: form.reportValidity() - paints the browser bubble on the FIRST invalid field and focuses it. This is the free UX most sites accidentally disable by hand-rolling everything.
  4. Add your own rules: field.setCustomValidity('Passwords do not match') marks the field invalid with your message; clear with field.setCustomValidity('') the moment the input becomes valid - wire both to the input event of the pair.
  5. Go fully custom: add novalidate to the form - the submit event now always fires even when invalid, and you render your own errors from validity states per field. Style with :user-invalid (not :invalid) so untouched required fields do not light up red on page load.

Frequently asked questions

Why does my field stay invalid after the user fixes it?

Because setCustomValidity() was called and never cleared. The custom message is sticky: the browser does not re-evaluate your cross-field logic on every keystroke, so the non-empty string keeps marking the field invalid no matter what the user types. Wire setCustomValidity('') (the empty string) to the input event - recheck your rule and clear the moment it passes. The empty string is the only thing that clears it.

What is the difference between :invalid and :user-invalid?

:invalid applies from the moment the page loads - so every empty required input is red before the user has touched anything, the wall-of-shame effect. :user-invalid applies only after the user has typed in or left the field, which matches when a human considers an error real. Use :invalid sparingly (e.g. after a failed submit) and :user-invalid for live typing feedback; it is supported in all modern browsers.

Why does my form never fire the submit event when invalid?

That is native validation doing its job: without novalidate, an invalid form refuses to submit and fires NO submit event - the browser just paints the bubble on the first bad field. If you want to intercept invalid submits in JS, listen for the invalid event per field (note: it does not bubble, capture phase or per-field listeners only), or add novalidate and take full ownership of the flow.

What are all the validity states a field can report?

Eight, each mapped to a defining attribute: valueMissing (required), typeMismatch (type=email/url), patternMismatch (pattern), tooLong and tooShort (maxlength/minlength), rangeUnderflow and rangeOverflow (min/max), stepMismatch (step) - plus valid, the all-clear flag. Check the individual flags instead of guessing: field.validity.tooShort is the difference between a useful message and a generic one.

Related tools