Topic 4.7
Built-in Validation
In one line
Attributes like required, pattern, min, max, step and minlength let the browser check values and block a bad submit with a helpful message, no JavaScript needed; but anyone can skip those checks, so the server must validate again.
Think of it like this
The ticket checker at a cinema door. They glance at your ticket before you go in: right film, right time. That saves everyone trouble. But the cinema still counts the money in the till later, because someone could have printed a fake ticket at home. Browser validation is the friendly door check; server validation is the count that can't be skipped.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Validation
- Checking that a value follows the rules before accepting it.
- Constraint
- One rule a value must follow, set by an attribute like
requiredormax. - required
- The field must have a value (or be ticked, or have an option picked).
- pattern
- A regular expression the whole value must match, like
[0-9]{6}. - Regular expression
- A short code that describes a shape of text, such as "six digits" or "letters only".
- ValidityState
- The
.validityobject on a control, with a true/false flag for each kind of problem. - setCustomValidity
- A JavaScript method that marks a field invalid with your own message, or valid again with
"". - novalidate
- A form attribute that turns off the browser's automatic blocking and messages.
- :user-invalid
- A CSS selector for fields that are invalid after the person has interacted with them.
Step by step
01Make a field required
Add one attribute. Try to submit empty and the browser blocks it, focuses the box and shows a message in the person's own language.
<label for="name">Name</label>
<input id="name" name="name" required>02Add rules for the shape of the value
Numbers get min, max and step; text gets minlength, maxlength and pattern. The title attribute's text is added to the pattern error message in some browsers, but don't rely on it; show the format in visible text as well.
<label for="age">Age</label>
<input id="age" name="age" type="number" min="18" max="99" required>
<label for="pin">PIN code (6 digits)</label>
<input id="pin" name="pin" inputmode="numeric" pattern="[0-9]{6}" required>
<label for="pw">Password (at least 8 characters)</label>
<input id="pw" name="pw" type="password" minlength="8" required>03What happens on submit
The browser checks every control in order. One failure is enough to stop the whole submit.
04Style errors at the right time
:user-invalid waits until the person has touched the field, so a fresh form isn't covered in red.
input:user-invalid {
border-color: crimson;
background: #fff0f0;
}
input:user-valid {
border-color: seagreen;
}05A rule HTML can't express
"Confirm password must equal password" compares two fields. setCustomValidity lets JavaScript plug that rule into the same system, so the browser blocks the submit and shows your message.
function checkMatch() {
again.setCustomValidity(pw.value === again.value ? "" : "Passwords do not match");
}
pw.addEventListener("input", checkMatch);
again.addEventListener("input", checkMatch);06Check again on the server
The browser's checks never reach the server, and anyone can switch them off. Your server code must repeat every rule and reject bad data with a clear error.
Try it yourself
- 1
Fix one field at a time
In the first example, change the email value to
asha@example.comand run it. Predict whichinvalid:line disappears. Then fix the others one by one untilform valid? true. - 2
Make the empty field pass
Remove
requiredfrom the name field. Run it: an empty optional field is valid, sofullname valueMissingbecomesfalse. - 3
Watch :user-invalid
In the second example's preview, click the email box, type
abc, then click elsewhere. The border turns red only now. Removenovalidatefrom the form and press Create account: the browser now blocks the submit and shows "Passwords do not match" or the email message.
Code & diagrams
`pw` is only 3 characters but not `tooShort`: length rules only apply after the person edits the field, never to values from the HTML or from scripts. Press Send to see the browser's own message.
Preview
Console
Nothing logged yet.Expected console output
fullname valueMissing: true
email typeMismatch: true
zip patternMismatch: true
age rangeUnderflow: true
qty stepMismatch: true
pw tooShort: false
invalid: fullname
invalid: email
invalid: zip
invalid: age
invalid: qty
form valid? false`:user-invalid` stays off until you type in the email box and leave it. With `novalidate`, the submit event fires even though the form is invalid, so your own code must decide what to do.
Preview
Console
Nothing logged yet.Expected console output
customError: true | message: Passwords do not match
email :invalid? true | :user-invalid? false
submitted despite errors (novalidate). Valid? false
after fixing, customError: falseBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Rely on the browser only
Remove required from a field in DevTools → Elements and submit, or send the form data with a script.
Break #2
Forget to clear a custom error
Call again.setCustomValidity("Passwords do not match") when they differ, but never call setCustomValidity("") when they match.
Break #3
A pattern the browser can't read
Write pattern="[a-z0-9_-]+" for a username.
Myth vs fact
Myth
If the form has required and pattern, the server can trust the data.
Fact
Anyone can bypass browser checks. The server must validate everything again.
Myth
pattern needs ^ and $ around it.
Fact
The browser already anchors it: the whole value must match. Adding them is harmless but unnecessary.
Myth
:invalid is the right selector for error styles.
Fact
:invalid matches before the person does anything, so a fresh form looks wrong. Use :user-invalid.
Myth
An empty email field fails type="email".
Fact
Empty optional fields always pass. Add required to demand a value.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
stepcounts from a step base:minif set, otherwise thevalueattribute, otherwise 0. So<input type="number" step="5" value="7">accepts 7, 12, 17…, while addingmin="0"makes 7 astepMismatch. Setminwhenever you usestep. - ▸
Elements barred from constraint validation are skipped entirely: disabled controls, readonly inputs, hidden inputs, buttons of type
button/reset, and anything inside a<datalist>.el.willValidatetells you whether a control takes part. - ▸
type="email"uses a deliberately simple rule from the HTML spec (roughlylocal@domainwith ASCII characters), not the full RFC 5322 grammar. It acceptsa@band rejects some legal but rare addresses. The only real proof an address works is sending a confirmation email. - ▸
badInputis true when the person typed something the browser can't turn into a value, such as12ein a number box. In that case.valueis"", so JavaScript can't see what was typed; checkvalidity.badInputinstead of assuming the box is empty. - ▸
validationMessageis localised by the browser (and differs between browsers), so don't compare it in code or tests. Messages set withsetCustomValidityare returned exactly as you wrote them.
Remember this
- 1
Validation means checking a value follows the rules before it's accepted. HTML calls these rules constraints. When a form is submitted, the browser checks every control; if any fails, it cancels the submit, focuses the first bad field and shows a message bubble ("Please fill out this field."). This is called constraint validation.
- 2
The rule attributes: **
required(must not be empty; for a checkbox it must be ticked, for a radio group one must be picked, for a select the value must not be empty).minlength/maxlength(number of characters).min/max(smallest and largest value for numbers, dates, times and ranges).step** (allowed increments, counted frommin:step="5"withmin="0"allows 0, 5, 10…;step="any"allows any decimals). **pattern(a regular expression**, a mini-language for text shapes, that the whole value must match:[0-9]{6}means exactly six digits). - 3
The
typeis a rule too.type="email"must look likename@domain,type="url"must be an absolute URL likehttps://…, andtype="number"must be a number. An empty optional field always passes:pattern,minandtypeonly judge values that exist, so addrequiredwhen a value is needed. - 4
Every control has a **
validityobject (its ValidityState**) that says exactly what's wrong:valueMissing,typeMismatch,patternMismatch,tooShort,tooLong,rangeUnderflow,rangeOverflow,stepMismatch,badInputandcustomError, plusvalid.form.checkValidity()returnstrue/false, andreportValidity()also shows the messages. JavaScript can add its own rule withsetCustomValidity("message")(a non-empty message makes the field invalid;""makes it valid again), such as "Passwords do not match". - 5
CSS can show the state:
:validand:invalidmatch right away, even before the person types, which would paint an empty required form red on page load. **:user-invalid** matches only after the person has interacted with the field (and left it or tried to submit), which is almost always what you want.:requiredand:optionalmatch too.novalidateon the form (orformnovalidateon a button) turns the browser's blocking off, typically because JavaScript will show its own messages (Topic 4.8). - 6
The server must validate too. Browser checks are for the person's convenience only. Anyone can remove
requiredin DevTools, send a request from a script, or use an old browser. Treat every value that arrives at the server as untrusted: check types, lengths and ranges again, and never put it into SQL or HTML without proper escaping. See/system-designfor API design and input validation at scale.
Explain it without notes
Why must the server validate data even when the form uses HTML validation?
List the main validation attributes and what each checks.
What is the difference between :invalid and :user-invalid?
How do you add a rule HTML attributes can't express, like matching passwords?
What do novalidate and formnovalidate do, and when would you use them?
Practice
Make a form where username is required, 3–15 characters, lowercase letters and digits only.
Add CSS so invalid fields get a red border only after the person has interacted with them, and a green border when valid.
Trade-offs
- ↔
Native validation messages are free, localised and accessible, but can't be styled and appear one at a time; custom messages (with
novalidate) match your design and can show all errors at once but need careful accessibility work (Topic 4.8 and Topic 7.5). - ↔
Strict patterns catch typos but can reject real data (names with apostrophes, international phone numbers); validate only what you really need.
Done when you can
I can use
required,minlength,maxlength,min,max,stepandpattern.I can read
validityto find out exactly what's wrong with a field.I can add a custom rule with
setCustomValidityand clear it.I can style errors with
:user-invalidinstead of:invalid.I can explain why the server must validate again.