Open the AICI registration page, or the IRCTC ticket-booking form, or the Google Form your school sent around for the annual sports meet. Every one of them asks you to type things into boxes: a name, an email, a 10-digit mobile number, a 6-digit PIN code. Now imagine what would happen if none of these sites checked what you typed. You could submit a mobile number with 7 digits, an email with no "@" in it, or a PIN code left completely blank — and the form would happily accept it and send it off. The OTP would never arrive at that broken phone number. The confirmation email would bounce. Somewhere on a server, a database row would sit there, permanently useless, because nobody caught the mistake at the one moment it was cheap to catch: the moment you were still looking at the form.
Validation is the set of rules a form applies to the data you type, before that data is allowed to leave your browser. This chapter is about how those rules are written, how the browser enforces them automatically, how you write your own rules in JavaScript when the browser's built-in rules aren't enough, and — just as important — why validation in your browser is never the whole story.
A form with no rules at all
Start with the simplest possible form: two text boxes and a button.
<form id="basicForm">
<label for="uname">Username</label>
<input type="text" id="uname" name="uname">
<label for="email">Email</label>
<input type="text" id="email" name="email">
<button type="submit">Register</button>
</form>
Nothing stops a visitor from clicking Register while both boxes are empty, or from typing "banana" into the email box. The browser has no idea these boxes are supposed to hold a username and an email address — as far as it's concerned, <input type="text"> accepts any sequence of characters, including zero characters. If we want the form itself to reject bad input, we have to tell the browser what "bad" means. That's what validation attributes are for.
Teaching the browser what "valid" means
HTML gives every <input> a small vocabulary of attributes that describe a rule. The browser checks these rules continuously, the instant you type, and again when you try to submit.
required— the field cannot be left empty.type="email",type="tel",type="number",type="date"— the value must look like the named kind of data. (type="email"checks for a rough email shape; it does not check the address actually exists.)minlength/maxlength— the shortest and longest allowed number of characters.min/max— the smallest and largest allowed number, for numeric or date types.pattern— a custom shape written as a regular expression, for rules none of the above cover.
Let's rebuild the registration form using these, for four Indian-context fields: a username, an email, a 10-digit mobile number, and a 6-digit PIN code.
<form id="regForm">
<label for="uname">Username</label>
<input type="text" id="uname" name="uname"
required minlength="4" maxlength="20">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
<label for="phone">Mobile number</label>
<input type="tel" id="phone" name="phone"
pattern="[6-9][0-9]{9}"
title="10-digit Indian mobile number starting with 6, 7, 8, or 9"
required>
<label for="pincode">PIN code</label>
<input type="text" id="pincode" name="pincode"
pattern="[1-9][0-9]{5}"
title="6-digit PIN code (cannot start with 0)"
required>
<button type="submit">Register</button>
</form>
No JavaScript at all has been written yet, and this form already rejects bad input. That's the first thing to understand about HTML5 validation: it is a feature of the browser itself, switched on by these attributes.
Reading the pattern rules digit by digit
The pattern attribute holds a regular expression: a compact notation for describing the shape a string must have. Two patterns appear above, and it's worth reading each one character by character, because pattern is the tool you'll reach for whenever required and type aren't specific enough.
[6-9][0-9]{9} for the mobile number:
[6-9]— exactly one character, and it must be 6, 7, 8, or 9. This matches how Indian mobile numbers are allocated: no active number begins with 0–5.[0-9]{9}— exactly nine more characters, each any digit 0–9.- Total: 1 + 9 = 10 characters, all digits, first digit restricted.
"9876543210"matches."5876543210"does not (starts with 5)."98765432100"does not (11 digits — one too many for{9}to allow).
[1-9][0-9]{5} for the PIN code:
[1-9]— one digit, 1 through 9 (Indian PIN codes never start with 0).[0-9]{5}— five more digits, any value.- Total: 6 digits.
"560001"(Bengaluru GPO) matches."060001"does not.
Whatever text you put in the title attribute becomes part of the message the browser shows when a pattern fails — without it, the browser's default message is a generic "Please match the requested format," which tells the user nothing about what format is expected.
What actually happens when you click Submit
Every input field keeps a live record of whether it currently satisfies its own rules — this record is called the field's validity state, and the browser updates it after every keystroke, not just at submit time. So by the time you click the button, the browser already knows exactly which fields are valid and which aren't; clicking Submit doesn't trigger new checking, it triggers a decision based on checks that were already running.
Suppose a student named Ravi fills in uname = "ravi_k", leaves email empty, and correctly fills in a valid phone and PIN code, then clicks Register. Here is the precise sequence:
- The browser looks at the validity state of every field in the form: uname (valid), email (invalid — empty but
required), phone (valid), pincode (valid). - Because at least one field is invalid, the browser cancels the submission — no network request is sent, and the page does not reload.
- The browser focuses the first invalid field in document order and shows that field's message bubble. Here, email comes before any other invalid field, so the cursor jumps to email and a bubble reading something like "Please fill out this field" appears beneath it.
A common misreading of this behaviour is that the browser "stops checking" once it hits the first problem, as though phone and pincode were never examined. That's not what happens: every field's validity was already known before the click, and only the display is limited to one message at a time, to avoid overwhelming the user with four bubbles at once. If Ravi fixes his email and clicks Register again, the browser re-reads all four validity states afresh; if a second field is now the first invalid one, that field's bubble appears next. The checking is complete every time — only the reporting is one-at-a-time.
Asking without showing: checkValidity() and reportValidity()
Sometimes you want to inspect validity from JavaScript instead of leaving everything to the browser's default bubble. Two methods exist on both individual fields and on the whole <form> element:
form.checkValidity()— returnstrueorfalse. Silent: it does not focus anything or show any message.form.reportValidity()— does the same check, but if something is invalid, it also focuses the first invalid field and shows its message bubble, exactly like a native submit attempt would. It returns the sametrue/false.
These are useful when you want to replace the browser's plain bubble with your own styled error text. To do that, you first add the novalidate attribute to the <form> tag — this switches off the browser's automatic blocking-and-bubble behaviour, handing you full control — and then call checkValidity() or reportValidity() yourself inside a submit listener:
<form id="otpForm" novalidate>
<label for="otp">Enter OTP</label>
<input type="text" id="otp" name="otp" required pattern="[0-9]{6}">
<span id="otpError" class="field-error"></span>
<button type="submit">Verify</button>
</form>
const otpForm = document.getElementById('otpForm');
const otp = document.getElementById('otp');
const otpError = document.getElementById('otpError');
otpForm.addEventListener('submit', function (event) {
event.preventDefault(); // novalidate means we must handle everything
if (otpForm.checkValidity()) {
otpError.textContent = '';
// safe to proceed, e.g. send otp.value to the server
} else {
otpError.textContent = otp.validity.patternMismatch
? 'OTP must be exactly 6 digits.'
: 'OTP is required.';
otp.focus();
}
});
Trace it: if the student types "12a45b", the browser's own rule engine has already marked otp invalid, and specifically its validity.patternMismatch flag is true (six characters were supplied, but they don't all match [0-9]). checkValidity() returns false, so the else branch runs, and because patternMismatch is true, the message reads "OTP must be exactly 6 digits." If the student instead submits an empty box, patternMismatch is false but the field is still invalid on the required rule, so the message falls through to "OTP is required."
Notice the difference from the earlier regForm example: regForm has no novalidate, so the browser's default blocking-and-bubble behaviour was left switched on and needed no JavaScript at all. otpForm deliberately adds novalidate because this example wants to replace that default behaviour with its own styled error span. Both are correct uses of the constraint validation system — you choose novalidate only when you intend to take over the reporting yourself.
Form validity as a Boolean AND
You've already met AND in Boolean logic: an AND expression is true only when every one of its inputs is true. A form's overall validity works exactly this way. For the four-field regForm:
validity(form) = validity(uname) AND validity(email)
AND validity(phone) AND validity(pincode)
For validity(form) to be true, every term on the right must independently be true. One false anywhere sinks the whole expression — which is exactly why Ravi's form was blocked even though three of his four fields were perfectly fine.
This AND relationship also exposes a real limitation of HTML5's built-in attributes: every rule we've written so far — required, pattern, minlength — looks at one field at a time. None of them can express a rule that compares two different fields to each other, such as "the password and confirm-password boxes must hold the same value." pattern checks a field's own shape; it cannot look sideways at a sibling field. For that, we need JavaScript.
Beyond single-field rules: comparing two fields
Let's extend the registration form with a password and a confirm-password field, plus a place to display an error message:
<form id="regForm">
<label for="uname">Username</label>
<input type="text" id="uname" name="uname"
required minlength="4" maxlength="20">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
<label for="phone">Mobile number</label>
<input type="tel" id="phone" name="phone"
pattern="[6-9][0-9]{9}"
title="10-digit Indian mobile number starting with 6, 7, 8, or 9"
required>
<label for="pincode">PIN code</label>
<input type="text" id="pincode" name="pincode"
pattern="[1-9][0-9]{5}"
title="6-digit PIN code (cannot start with 0)"
required>
<label for="pwd">Password</label>
<input type="password" id="pwd" name="pwd" required minlength="8">
<label for="pwd2">Confirm password</label>
<input type="password" id="pwd2" name="pwd2" required minlength="8">
<div id="errorBox" role="alert"></div>
<button type="submit">Register</button>
</form>
Now the matching JavaScript, referencing exactly the ids declared above:
const form = document.getElementById('regForm');
const pwd = document.getElementById('pwd');
const pwd2 = document.getElementById('pwd2');
const errorBox = document.getElementById('errorBox');
form.addEventListener('submit', function (event) {
errorBox.textContent = '';
// This line only runs at all once every HTML5 constraint on
// uname, email, phone, pincode, pwd and pwd2 has ALREADY passed --
// regForm has no "novalidate", so an invalid field would have
// blocked submission (and the browser's own bubble would have
// appeared) before the "submit" event was even dispatched.
if (pwd.value !== pwd2.value) {
event.preventDefault();
errorBox.textContent = 'Passwords do not match.';
pwd2.focus();
return;
}
// Every check has passed. We do nothing further here --
// by not calling preventDefault(), we let the submission
// that was already underway continue to the server.
});
Walk through a concrete case. Suppose the fields hold: uname = "priya_9" (7 characters, within 4–20 → valid), email = "priya9@gmail.com" (valid email shape), phone = "9876543210" (starts with 9, ten digits total → matches [6-9][0-9]{9}), pincode = "560001" (starts with 5, six digits → matches [1-9][0-9]{5}), pwd = "MyPass123" (9 characters → satisfies minlength="8"), and pwd2 = "MyPass12" (8 characters → on its own, this also satisfies minlength="8").
Every single HTML5 constraint here is individually satisfied — six terms in our Boolean AND, all true — so the browser lets the submit event fire and our handler runs. But "MyPass123" !== "MyPass12": these are different strings (one is 9 characters, the other 8). No pattern or minlength attribute could ever have caught this, because HTML5 constraints never compare one field's value against another's. Our custom check catches it, calls preventDefault() to cancel the submission that native validation had already allowed through, and writes a message into errorBox. This is exactly the gap that motivates writing JavaScript validation at all: not to replace HTML5's per-field rules, but to add cross-field rules HTML5 has no vocabulary for.
One more detail worth being precise about: this handler never calls form.submit(). It doesn't need to — when the handler simply returns without calling preventDefault(), the browser continues the submission that was already in progress. This distinction matters because form.submit() and form.requestSubmit() are not interchangeable. form.submit() sends the form directly and skips both the submit event and constraint validation entirely — calling it from your own code would bypass the very checks this section just built. form.requestSubmit(), added specifically to close that gap, behaves like a genuine click on the submit button: it dispatches submit and re-runs every constraint. So if you ever need to trigger a submission from code — for instance, after an asynchronous check like confirming a username isn't already taken — call form.requestSubmit(), never form.submit().
Showing state with CSS: :valid and :invalid
Every field the browser is validating also carries a live CSS state, matched by the pseudo-classes :valid and :invalid:
input:invalid {
border: 2px solid #d93025;
}
input:valid {
border: 2px solid #1a7f37;
}
There's a well-known trap here: a required field that the user hasn't touched yet is already :invalid the moment the page loads (an empty required box fails its own rule immediately), so a plain rule like the one above paints every empty required field red before the student has even had a chance to type. The usual fix is to combine :invalid with :not(:-shown), which only matches once the field has actual content and its text has disappeared:
input:invalid:not(:-shown) {
border: 2px solid #d93025;
}
Now a field only turns red once the student has typed something invalid into it — not simply for being empty on first load.
The misconception to unlearn: client-side validation is not security
It's tempting to think that once a form rejects bad phone numbers and mismatched passwords in the browser, the server is protected. This is false, and it's worth being precise about why. Every rule discussed in this chapter — required, pattern, the password-match check — runs as JavaScript and HTML inside a browser that the user themselves controls. A visitor can open their browser's developer tools and delete the required attribute, or simply skip the browser entirely and send a request straight to the server using a command-line tool, with any data they like, correctly formatted or not. Client-side validation never even runs in that case — there is no browser page to run it.
What client-side validation is genuinely good for is speed and feel: catching a typo the instant it happens, without a wasted round trip to the server and back. What it can never do is guarantee that the data arriving at the server is well-formed, because the server has no way to know whether the request in front of it came from your validated form or from someone who wrote their own request by hand. That's why every serious system re-checks all the same rules again, in server-side code, no matter how thorough the browser-side checks were. The rule to remember: validate in the browser for the user's convenience; validate on the server for the application's safety. Skipping the second one because the first one exists is one of the most common real-world security mistakes in web development.
The full validation flow
Follow the diagram left to right, top to bottom: the browser's own HTML5 constraints run first and can block submission entirely, without any JavaScript. Only once those pass does the submit event reach your code, where cross-field checks like password matching run. Only once those pass does data leave the browser at all — and even then, the diagram's last box is not optional: the server repeats its own checks, because it can never trust that the browser's checks actually ran.
Summary
- A plain
<input>with no attributes accepts any text, including empty text. Validation attributes teach the browser what "acceptable" means for that field. required,type,minlength/maxlength,min/max, andpatternare checked continuously by the browser and, by default (unlessnovalidateis present), block form submission and show a message automatically — no JavaScript required.- A form's overall validity is the Boolean AND of every field's individual validity: one invalid field is enough to block the whole form.
- When the browser blocks submission, it focuses only the first invalid field in document order, but it has already evaluated every field's validity, not just the first one.
checkValidity()asks silently;reportValidity()asks and also shows the native UI. Both are typically paired withnovalidatewhen you want to take over error display yourself.- HTML5 constraints only ever look at one field at a time. Rules that compare two fields — like confirming a password — need a JavaScript
submitlistener with your own comparison logic. - Use
event.preventDefault()to stop a submission your custom check has rejected. Never callform.submit()to resubmit programmatically — it skips validation entirely; useform.requestSubmit()if you need to trigger a submission from code. - Client-side validation improves the user's experience; it provides no security at all, because a user fully controls their own browser. Every rule enforced in the browser must be enforced again on the server.
Check your understanding
- A field has
pattern="[1-9][0-9]{5}". Would the value"123456"pass? Would"012345"? Explain each using the character-by-character reading you learned in this chapter. - A form has three fields, all currently valid, and a fourth field that is empty and
required. What doesform.checkValidity()return? What would clicking the submit button do, assuming the form does not havenovalidate? - Why can no combination of
pattern,required, andminlengthever enforce "this field must equal that other field"? What has to be used instead? - A developer writes a password-match check, and afterwards calls
form.submit()to send the data. A teammate says this is a bug. What goes wrong, and what should be called instead? - A site validates the mobile-number field thoroughly with JavaScript before allowing submission. Does this mean the server can safely assume every mobile number it receives has 10 digits and starts with 6-9? Justify your answer.
Practice Exercises
Now it is time to practice! Complete these challenges to solidify your understanding:
- Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
- Exercise 2: Find a real-world example where web forms and validation: user input done right is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
- Exercise 3: Create a mind-map connecting web forms and validation: user input done right to at least 3 other topics you have studied.