Command Palette

Search for a command to run...

Hectal
PHASE 4Intermediate ~21 min· topic 7 of 8

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 required or max.
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 .validity object 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.

index.htmlwhole filehtml
<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.

index.htmlwhole filehtml
<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.

What happens on submitdiagram
Rendering diagram…

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.

style.csswhole filecss
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.

app.jswhole filejavascript
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.

Check again on the serverdiagram
Rendering diagram…

Try it yourself

  1. 1

    Fix one field at a time

    In the first example, change the email value to asha@example.com and run it. Predict which invalid: line disappears. Then fix the others one by one until form valid? true.

  2. 2

    Make the empty field pass

    Remove required from the name field. Run it: an empty optional field is valid, so fullname valueMissing becomes false.

  3. 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. Remove novalidate from the form and press Create account: the browser now blocks the submit and shows "Passwords do not match" or the email message.

Code & diagrams

Every kind of validity problem New tab

`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

Press Run to see the page.

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
Custom rules, :user-invalid and novalidate Baseline 2023+ New tab

`: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

Press Run to see the page.

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: false

Break 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.

terminal
$ Server receives
── what you'll see ──
fullname=&email=not-an-email&age=-5

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.

terminal
$ the page shows
── what you'll see ──
"Passwords do not match" keeps appearing on submit even after both boxes are identical

Break #3

A pattern the browser can't read

Write pattern="[a-z0-9_-]+" for a username.

terminal
$ Console
── what you'll see ──
Pattern attribute value [a-z0-9_-]+ is not a valid regular expression: ... Invalid character in character class

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.

  • ▸

    step counts from a step base: min if set, otherwise the value attribute, otherwise 0. So <input type="number" step="5" value="7"> accepts 7, 12, 17…, while adding min="0" makes 7 a stepMismatch. Set min whenever you use step.

  • ▸

    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.willValidate tells you whether a control takes part.

  • ▸

    type="email" uses a deliberately simple rule from the HTML spec (roughly local@domain with ASCII characters), not the full RFC 5322 grammar. It accepts a@b and rejects some legal but rare addresses. The only real proof an address works is sending a confirmation email.

  • ▸

    badInput is true when the person typed something the browser can't turn into a value, such as 12e in a number box. In that case .value is "", so JavaScript can't see what was typed; check validity.badInput instead of assuming the box is empty.

  • ▸

    validationMessage is localised by the browser (and differs between browsers), so don't compare it in code or tests. Messages set with setCustomValidity are returned exactly as you wrote them.

Remember this

  1. 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. 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 from min: step="5" with min="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. 3

    The type is a rule too. type="email" must look like name@domain, type="url" must be an absolute URL like https://…, and type="number" must be a number. An empty optional field always passes: pattern, min and type only judge values that exist, so add required when a value is needed.

  4. 4

    Every control has a **validity object (its ValidityState**) that says exactly what's wrong: valueMissing, typeMismatch, patternMismatch, tooShort, tooLong, rangeUnderflow, rangeOverflow, stepMismatch, badInput and customError, plus valid. form.checkValidity() returns true/false, and reportValidity() also shows the messages. JavaScript can add its own rule with setCustomValidity("message") (a non-empty message makes the field invalid; "" makes it valid again), such as "Passwords do not match".

  5. 5

    CSS can show the state: :valid and :invalid match 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. :required and :optional match too. novalidate on the form (or formnovalidate on a button) turns the browser's blocking off, typically because JavaScript will show its own messages (Topic 4.8).

  6. 6

    The server must validate too. Browser checks are for the person's convenience only. Anyone can remove required in 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-design for API design and input validation at scale.

Explain it without notes

01

Why must the server validate data even when the form uses HTML validation?

02

List the main validation attributes and what each checks.

03

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

04

How do you add a rule HTML attributes can't express, like matching passwords?

05

What do novalidate and formnovalidate do, and when would you use them?

Practice

01

Make a form where username is required, 3–15 characters, lowercase letters and digits only.

02

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, step and pattern.

  • I can read validity to find out exactly what's wrong with a field.

  • I can add a custom rule with setCustomValidity and clear it.

  • I can style errors with :user-invalid instead of :invalid.

  • I can explain why the server must validate again.