Every browser can validate a form without a line of JavaScript. The constraints live in the markup — required, type, pattern, min, max — and the browser refuses to submit until they are satisfied, showing a message in the visitor's own language.
Which constraints does the browser enforce?
<input type="text" name="name" required>
<input type="email" name="email" required>
<input type="number" name="quantity" min="1" max="99" step="1">
<input type="text" name="code" pattern="[A-Z]{2}[0-9]{3}" required>
<input type="url" name="website" required>
<input type="text" name="username" minlength="3" maxlength="20">
<textarea name="bio" minlength="20" maxlength="500"></textarea>
The rules are per-type. An email field checks that the value looks like an address. A number field is checked against min, max and step. A url field needs a scheme. The pattern attribute takes a regular expression, anchored to the whole value, so a pattern must describe the entire entry rather than part of it.
Two constraints deserve care. required on a checkbox means it must be ticked, not merely present. And step is a multiple of the base value: with step="5", a value of 7 is invalid, while 10 and 15 are fine.
What does the visitor see when it fails?
The browser blocks the submission, focuses the offending field, and shows a message. The exact wording and position depend on the browser, which is why you should never depend on the precise text. The behaviour itself — no navigation, the field focused, an explanation — is consistent everywhere.
type="email" with the value "not-an-email"
-> the browser refuses to submit and reports that the value is not an email address
type="number" with the value "12" and max="10"
-> the value must be less than or equal to 10
pattern="[A-Z]{2}[0-9]{3}" with the value "ab123"
-> the value must match the requested format
Validation on the client is a convenience, not a security measure. Anyone can bypass it, so the server must check every value again. What the client-side check buys you is fewer round trips and a faster correction for the people who make a typo.
Can you write your own messages?
Yes, and it is worth doing when the default message is vague. The setCustomValidity method attaches a message to a control, and the browser shows it instead of the default one.
const code = document.querySelector('#code');
code.addEventListener('input', () => {
code.setCustomValidity('');
const value = code.value.trim();
if (value && !/^[A-Z]{2}[0-9]{3}$/.test(value)) {
code.setCustomValidity('Use two capital letters followed by three digits, like AB123.');
}
});
Two rules make this work. Clear the custom message first, on every input, or it sticks after the visitor fixes the value. And call reportValidity() on the form to display it immediately, because setting the message alone does not show it until the next validation attempt.
How do you show which field is wrong?
The browser exposes the validity state of each control, so CSS can react to it. The :user-invalid pseudo-class applies only after the visitor has interacted with the field, which avoids the insulting effect of a red box appearing on an untouched form.
input:user-invalid {
border-color: #b3261e;
}
input:user-valid {
border-color: #2e7d32;
}
.field-error {
color: #b3261e;
font-size: 0.875rem;
}
Two cautions. The :invalid pseudo-class matches a field that is empty and untouched, so styling it directly turns every new form red before the visitor has typed anything — :user-invalid avoids that. And a red border on its own is not enough: colour alone excludes anyone who cannot distinguish red from green, so pair it with a message or an icon.
Why does the server have to validate too?
Because client-side validation is a suggestion, not a gate. The rules live in the page the visitor downloaded, and that page is not the copy the server checks. Anyone can submit a request without loading the form at all, using a script or the browser's developer tools.
what the browser checks what the server must check
-------------------------------- --------------------------------
the value looks like an email the address exists and can receive mail
the number is in range the number is in range
the pattern matches the pattern matches
the field is not empty the field is not empty
The two layers do different jobs. The client-side check is a kindness: it tells the visitor about a typo immediately, without a round trip. The server-side check is the authority: it is the only one that cannot be bypassed. A good rule is to treat the browser's validation as a first draft of the validation, and to write the real one on the server with the same constraints and the same messages.
How do you turn the built-in validation off?
The novalidate attribute on the form suppresses the browser's automatic checking, which is what you do when you are building your own validation with your own messages. The formnovalidate attribute does the same for a single button, which is how you offer a "save without validating" action next to a strict one.
<form action="/draft" method="post" novalidate>
...
<button type="submit" formnovalidate>Save draft</button>
<button type="submit">Publish</button>
</form>
Use it sparingly. The browser's messages are already localised, already accessible, and already familiar to the visitor. Replacing them with your own means implementing the whole experience: showing the error, moving focus to the field, describing what is wrong in text, and doing it in every language your visitors use.
Which values does the browser consider empty?
One detail catches people out: a field containing only spaces counts as filled for required. The browser is not trimming your input before it checks.
<input type="text" name="name" required>
Submit that with a single space in it and the browser will accept it. If spaces are not valid input, strip them yourself before validating — on the server, where it cannot be bypassed, and in the browser if you want the feedback to be immediate.
Quick reference
- Constraints live in the markup:
required,type,pattern,min,max,step,minlength. - A
patternmust match the whole value, so anchor it. - Client validation is a convenience; the server must validate again.
setCustomValidity('')clears a custom message — do it on every input.- Use
:user-invalidrather than:invalidto avoid marking untouched fields as wrong. - Never rely on colour alone to signal an error.
Try it yourself: build a form with an email, a number with a range and a pattern field, then try to submit it empty. Note which field the browser focuses. Now add a custom message for the pattern field and check that it appears — and that it disappears once the value is corrected.