What you'll learn
Quick Answer
required, type, pattern, min, max and maxlength give you validation with accessible messages and no JavaScript. Use them first, add JavaScript only for rules they cannot express, and always validate again on the server.
What you get for free
<form>
<label for="email">Email</label>
<input id="email" name="email" type="email" required>
<label for="age">Age</label>
<input id="age" name="age" type="number" min="13" max="120" required>
<label for="roll">Roll number</label>
<input id="roll" name="roll" pattern="[0-9]{6}"
title="Six digits" required>
<button>Submit</button>
</form>
That validates on submit, blocks it while invalid, focuses the first problem field and shows a message — with no JavaScript, and announced correctly by screen readers.
The attributes worth knowing: required, type (email, url, number, tel, date), min and max, minlength and maxlength, step, and pattern for a regular expression.
type does double duty on mobile: type="email" and type="tel" bring up the appropriate keyboard, which is a real usability gain that JavaScript validation cannot provide.
Every input needs a <label> with a matching for. Placeholder text is not a label — it disappears on typing and is poorly announced.
Styling validity without shouting too early
input:invalid { border-color: #dc2626; }
input:valid { border-color: #16a34a; }
The problem: an empty required field is invalid immediately, so the form is covered in red before the user types anything.
:user-invalid fixes it by applying only after the user has interacted with the field:
input:user-invalid { border-color: #dc2626; }
input:user-invalid + .error { display: block; }
That matches what people expect — tell me after I have tried, not before I have started.
Do not rely on colour alone. A red border is invisible to some users, so pair it with a text message and an icon.
Custom messages and rules
Default browser messages are generic and not translatable. Override them with the Constraint Validation API:
const roll = document.getElementById("roll");
roll.addEventListener("input", () => {
roll.setCustomValidity(
roll.validity.patternMismatch
? "Roll number must be exactly six digits"
: ""
);
});
Setting a non-empty string marks the field invalid with that message; setting "" clears it. You must clear it, or the field stays permanently invalid — a common bug.
The same API handles rules HTML cannot express, such as comparing two fields:
confirm.setCustomValidity(
confirm.value === password.value ? "" : "Passwords do not match"
);
Useful properties on field.validity: valueMissing, typeMismatch, patternMismatch, rangeOverflow and tooShort, so you can tailor the message to the actual failure.
Client validation is not security
The point that matters most.
Everything above is a convenience for honest users. It tells someone they mistyped their email before a round trip. It stops nobody who does not want to be stopped.
Anyone can open developer tools and delete the required attribute, or skip your page entirely and POST directly to your endpoint with any data at all. HTML validation exists in the browser, and the browser is under the user's control.
Every rule must be enforced again on the server. Not "also" — the server check is the real one, and the client check is the nicety. See designing a REST API for returning validation errors properly.
This is not hypothetical. Assuming client validation held is how invalid data reaches production databases, and how negative quantities end up in orders.
Form usability details that matter
- Use
autocompleteattributes —autocomplete="email","given-name","one-time-code". Browsers and password managers fill these correctly, which measurably increases completion. - Do not disable paste on password fields. It breaks password managers and pushes people towards weaker passwords.
- Validate email loosely. Strict regular expressions reject valid addresses.
type="email"is a sensible check; only sending a message proves an address works. - Keep error messages next to the field and describe how to fix it — "Password needs at least 8 characters" beats "Invalid input".
- Never clear the form on a failed submit. Re-typing everything is the fastest way to lose a user.
