Form validation
Validate browser form fields with required, type, pattern, validity, setCustomValidity, custom messages, and accessible error UI while still trusting the server for final checks.
- 01Use built-in constraintsCombine
required, input types,pattern,min,max,step, and length attributes. - 02Read and change validityInspect
ValidityState, comparecheckValidity()withreportValidity(), and clear custom errors. - 03Design humane errorsBuild custom messages, summaries, focus movement,
aria-invalid, and live regions.
The bouncer at the form door
Form validation is the set of checks that decide whether a form value is ready to accept. A signup form might require an email, a booking form might require a date after today, and a coupon box might accept only a code shaped like SAVE20.
Browsers have a built-in system for many of these checks: the Constraint Validation API. You describe constraints with HTML attributes, then JavaScript reads properties such as input.validity, calls methods such as checkValidity(), and adds special rules with setCustomValidity().
The bouncer does not just say “no”. They look down a checklist: ticket present, ticket format right, age in range. The browser does the same for a form control. Each failed rule gets its own flag, which makes debugging much easier than one mysterious invalid state.
- In real life: You must have a ticket
- In JavaScript:
required: the field cannot be empty - In real life: The ticket must look like tonight's format
- In JavaScript:
patternand inputtypechecks - In real life: Age limits at the door
- In JavaScript:
min,max, andstepfor numbers and dates - In real life: A tick or cross per rule
- In JavaScript:
validity: booleans such asvalueMissingandtypeMismatch
Where the analogy stops: A real bouncer can judge context and stop attackers. Browser validation can be bypassed, so your server still has to validate every important value.
Client-side validation is kind. It saves people a round trip and helps them fix small mistakes while the field is still in front of them. It is not proof that the data is safe. Anyone can send a request without using your page at all.
- In real life: Receptionist points out a missing signature
- In JavaScript: The browser warns about obvious mistakes immediately
- In real life: Manager adds a rule for tonight
- In JavaScript:
setCustomValidity('Passwords must match') - In real life: Security verifies the ID later
- In JavaScript: The server repeats trusted validation
Where the analogy stops: The receptionist improves the visit, but cannot be the final authority. Never store, charge, or trust data only because the browser accepted it.
required, pattern & input types
STEP THROUGHStart with HTML. The browser already knows how to validate many common rules, and those rules also help mobile keyboards, password managers, and assistive technology understand the field.
requiredmeans the value cannot be empty. An empty required control hasvalidity.valueMissing.type="email",type="url",type="number", and date-like types add format or parsing rules. A bad email or URL givestypeMismatch.patterntests a regular-expression-like pattern against the whole value. It is ignored when the field is empty unlessrequiredalso fails. The Regular expressions lesson comes later; for now, read[A-Z]3as “three uppercase letters.”min,max, andstepvalidate ranges for numbers and dates.minlengthandmaxlengthvalidate text length; browsers generally settooShortafter the user edits the value.
A control can also opt out of validation. input.willValidate is false for controls such as disabled inputs, readonly text-like controls, and type="hidden" inputs. That is the browser saying “this control is not standing in the validation line.”
Pick a scenario, predict the flags, then step through the checklist.
script
const validity = check(field);console.log(validity.valueMissing);console.log(validity.patternMismatch);console.log(validity.valid);Notice the empty optional pattern case: the pattern flag stays false. If empty should be a problem, add required. If the field is optional, let people leave it blank without seeing a format error.
Read the validity checklist
INTERACTIVEEvery form control that participates in validation exposes validity, a ValidityState object. It has one boolean for each kind of failure and a final valid boolean. The final value is true only when every failure flag is false.
The dashboard below reads the browser’s real flags after the component mounts and whenever you type. It also shows validationMessage. That message is generated by the browser, so its exact words can vary by browser and language. Product copy should usually come from your own message map.
<form> <input name="name" required> <input name="email" type="email" required> <input name="site" type="url"> <input name="tickets" type="number" min="1" max="10" step="2"> <input name="code" minlength="3" maxlength="8" pattern="[A-Z]{3}[0-9]{2}"> <input name="day" type="date" min="2026-01-01"></form>The table reads each control's real validity object and browser validationMessage after the component mounts or the user edits a field. The message text can differ by browser and language.
Try these tiny investigations: leave Name empty to see valueMissing, type abc in Email to see typeMismatch, type 7 in Tickets to see stepMismatch, and type ab in Code after editing it to see the length rule.
- An empty required field
- abc in
type=email - 7 in
min=0 step=5 - A password confirmation message from
setCustomValidity
Sort each situation by the flag it turns on.
- 12 in a text field with
minlength=3 - abcdef in a field with
maxlength=4 - ab12 in a field with
pattern="[A-Z]{2}[0-9]{2}"
Now sort text-specific failures by the flag they turn on.
validity & setCustomValidity
INTERACTIVEBuilt-in attributes cover individual fields well, but real products often have rules that compare fields: passwords must match, an end date must come after a start date, or a username cannot be on a reserved list. That is where setCustomValidity(message) enters.
Any non-empty message marks that one control invalid and turns on validity.customError. Calling setCustomValidity("") clears the custom error. The empty string is not a decoration; it is the off switch.
confirm.addEventListener("input", () => { const match = password.value === confirm.value; confirm.setCustomValidity(match ? "" : "Passwords must match");}); form.checkValidity(); // boolean, fires invalidform.reportValidity(); // boolean, asks browser to show UIType matching passwords, then try the buttons.
setCustomValidity("").| Method | Returns | Main job |
|---|---|---|
input.checkValidity() / form.checkValidity() | Boolean | Runs validation and fires invalid on invalid controls without showing native UI. |
input.reportValidity() / form.reportValidity() | Boolean | Runs validation and asks the browser to report problems to the user. |
input.setCustomValidity(message) | undefined | Sets or clears one custom error. Any non-empty message means invalid. |
The invalid event fires on invalid controls during these checks. It does not bubble like many events, so listen in the capture phase on the form if you want one counter or logger.
Custom error messages
INTERACTIVEBrowser bubbles are useful for quick prototypes, but production interfaces often need consistent wording, summaries, translations, and layout. Add novalidate to the form to turn off automatic bubbles, then call checkValidity() yourself and render your own messages.
A good message tells the user how to fix the field: Use a real email address, like ada@example.com
is better than Invalid input.
Use the same validity flags, but translate them into friendly instructions.
<form novalidate> <input id="email" type="email" required aria-describedby="email-error"> <p id="email-error"></p> <button>Join</button></form> form.addEventListener("submit", (event) => { event.preventDefault(); if (!form.checkValidity()) showFriendlyErrors();});Submit to render custom messages instead of browser bubbles.
novalidate, so the submit handler runs and renders friendly messages from the same validity flags.After the first failed submit, update errors on input or blur. Before the first submit, many teams avoid showing red errors while the person is still typing. The newer CSS selector :user-invalid follows that idea in supporting browsers; :invalid can match before the user touches the field.
Accessible errors
INTERACTIVEAn error is not accessible just because it is red. People using a keyboard, screen reader, zoom, or voice control need programmatic relationships between the field, the message, and the summary.
- Set
aria-invalid="true"on fields that currently have errors. - Use
aria-describedbyto point to the visible error text. - Put a summary near the top with links to invalid fields.
- Move focus to the first invalid field, or the summary, after submit.
- Use a polite live region so new messages can be announced.
<div role="alert" aria-live="polite" id="error-summary"></div><input id="email" required aria-invalid="true" aria-describedby="email-error"><p id="email-error">Enter your email address.</p>Enter your email address.
When the error is shown, aria-invalid tells assistive tech the field is invalid, aria-describedby connects the field to the message, and the summary uses a live region.
The form events lesson introduced focus movement and submit handling. Validation combines those skills: catch the failed submit, show a summary, focus a useful target, and keep the messages connected while the user fixes each field.
Where you will use this
In a signup form, you might use type="email" required for the email, minlength="8" for a password, setCustomValidity() for password confirmation, and a custom summary on submit. In checkout, range and step rules can validate quantities. In settings, custom validity can block a username that is reserved by your app.
Prefer built-in constraints first, add custom validity only for rules the browser cannot express, and always validate again on the server. The browser improves the experience; the server protects the data.
Once a form passes validation, the next lesson, FormData, shows how to collect the values with new FormData(form) and prepare them for sending.
Common misconceptions
- “The browser accepted it, so it is safe.” No. The server must validate every trusted rule.
- “pattern makes a field required.” No. Empty optional fields skip
pattern. - “A custom error clears itself.” No. You must call
setCustomValidity(""). - “validationMessage is my product copy.” It is browser and locale text. Map flags to your own copy when wording matters.
- “A red border is enough.” Accessible errors need text, relationships, summaries, and focus.
| Shortcut | Better habit |
|---|---|
Only use JavaScript if checks | Use HTML constraints so the browser, keyboard, and accessibility tree understand the rule. |
| Show every error immediately | Wait until submit or user interaction, then update clearly. |
| Trust client-side validation | Validate on the server before saving, charging, or authenticating. |
| Write one generic error | Map the specific validity flag to a helpful next action. |
Practice exercises
4 EXERCISESA required text input is empty. Type the validity flag that becomes true.
console.log("valueMissing");console.log("valueMissing");An empty required field fails the required rule, so the flag is valueMissing.
Read the solution pattern, then answer: what exact string clears a custom validity error?
const password = form.elements.password;
const confirm = form.elements.confirm;
function validatePasswords() {
const message = password.value === confirm.value ? "" : "Passwords must match";
confirm.setCustomValidity(message);
}
password.addEventListener("input", validatePasswords);
confirm.addEventListener("input", validatePasswords);const password = form.elements.password;
const confirm = form.elements.confirm;
function validatePasswords() {
const message = password.value === confirm.value ? "" : "Passwords must match";
confirm.setCustomValidity(message);
}
password.addEventListener("input", validatePasswords);
confirm.addEventListener("input", validatePasswords);The confirmation field owns the custom error. When the values match, setCustomValidity("") clears customError; otherwise the message keeps the field invalid.
You want the browser to show its built-in validation UI for a field. Which method do you call?
reportValidity() can show the browser's validation UI. checkValidity() is the quieter boolean check, though it still fires invalid.
After a submit fails, which invalid field should receive focus first?
function showError(input, message) {
const error = document.querySelector("#" + input.id + "-error");
input.setAttribute("aria-invalid", message ? "true" : "false");
input.setAttribute("aria-describedby", error.id);
error.textContent = message;
}function showError(input, message) {
const error = document.querySelector("#" + input.id + "-error");
input.setAttribute("aria-invalid", message ? "true" : "false");
input.setAttribute("aria-describedby", error.id);
error.textContent = message;
}The field points to its error text with aria-describedby, announces invalid state with aria-invalid, and after a failed submit your handler should focus the first invalid field or an error summary that links to it.
Check your understanding
7 QUESTIONSQuestion 1 of 7What happens when an optional text field has
pattern="[A-Z]{3}"and its value is empty?Choose an answer to see the explanation.
Question 2 of 7What does the custom validity snippet print?
Read the code, then predictconst message = "Passwords must match"; console.log(message === "" ? "valid" : "customError");Choose an answer to see the explanation.
Question 3 of 7Which method returns a boolean and fires
invalidon invalid controls, but does not show the browser bubble UI?Choose an answer to see the explanation.
Question 4 of 7What does the first-flag helper print?
Read the code, then predictfunction firstFlag(flags) { return flags.valueMissing ? "valueMissing" : flags.valid ? "valid" : "other"; } console.log(firstFlag({ valueMissing: true, valid: false }));Choose an answer to see the explanation.
Question 5 of 7Which controls usually have
willValidateas false?Choose an answer to see the explanation.
Question 6 of 7Why use
novalidatewhen building custom error UI?Choose an answer to see the explanation.
Question 7 of 7What does the client-and-server snippet print?
Read the code, then predictconst serverMustCheck = true; const browserHelpful = true; console.log(browserHelpful && serverMustCheck ? "both" : "browser only");Choose an answer to see the explanation.
Key takeaways
- HTML constraint attributes give you powerful validation before you write custom JavaScript.
validityis a checklist of booleans;validis true only when no rule fails.setCustomValidity("message")createscustomError;setCustomValidity("")clears it.checkValidity()tests;reportValidity()tests and asks the browser to report.- Custom UI must be accessible, and the server must validate again.
Form validation is the browser and app checking form values against rules, helping users fix mistakes before trusted server validation runs.
Up next: FormData.