JavaScript Form Validation Table
| Piece | What it does | Field note |
|---|---|---|
field.validity.valid | The master flag | false means ANY of the constraint states fired - check the individual states for why |
validity.valueMissing / typeMismatch / patternMismatch / tooShort / rangeUnderflow / stepMismatch | WHY it failed | The states map 1:1 to HTML attributes: required / type / pattern / minlength / min / step - no guessing |
field.checkValidity() | The silent check | Returns a boolean, paints nothing - fires the invalid event on the field when false |
form.reportValidity() | The native UI pass | Paints the browser error bubble on the FIRST invalid field and focuses it - free UX most sites disable |
field.setCustomValidity('msg') | Your own rule | Any 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-invalid | Styling 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 form | Turns OFF native blocking | The submit event always fires and YOU own the error UI - without it, an invalid form just refuses to submit with zero events |
the invalid event | The one hook | Fires per-field on a failed checkValidity() - the hook for rendering your own error text; it does NOT bubble |
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
- 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.
- 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.
- 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.
- 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.
- 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.