010: HTML Validation
Learning outcomes
By the end of this lesson, you can choose and apply required, length, range, type, and pattern constraints; test native browser validation systematically; tell constraint validation apart from markup conformance; and explain why every client-side check has to be repeated on the server.
Prerequisites and retrieval
Start with 009's complete form. Identify the name and value for every control, and explain why a user can change even a predefined value before the request reaches the server. Also recall the distinction between the Nu checker and today's topic: Nu finds markup mistakes, while browser constraint validation checks the values a user entered.
Terminology
- Constraint: A validation rule attached to a control through attributes such as
required,minlength, andpattern. — Source: WHATWG: Constraints - Constraint validation: The browser mechanism that checks whether controls satisfy the constraints that apply to them. — Source: WHATWG: Constraint validation API
- Valid/invalid: Whether a control currently has no validity-state failures (
validity.valid). — Source: WHATWG: ValidityState - Type mismatch: A validity failure that occurs when the entered value does not match the syntax required by its type, such as
emailorurl. — Source: WHATWG: Type mismatch - Pattern: The
patternattribute, which compiles a JavaScript regular expression and matches it against the entire value. — Source: WHATWG: pattern attribute - Range: The
min,max, andstepbounds that constrain numeric and temporal input values. — Source: WHATWG: min/max attributes - Client-side validation: Constraint checks carried out in the browser before submission completes. — Source: MDN: Client-side form validation
- Server-side validation: Authoritative checks performed after receipt; client-side checks provide convenience, not security. — Source: MDN: Client-side form validation
- Conformance validation: Checking markup against authoring rules with a checker. This is separate from checking entered values. — Source: Nu HTML Checker
- Validity states: "Validity states include valueMissing, typeMismatch, patternMismatch, tooShort, tooLong, rangeUnderflow, rangeOverflow, stepMismatch, badInput, customError." — Source: WHATWG: ValidityState
- Barred from constraint validation: "Controls such as disabled, hidden, or inside datalist are barred and not validated." — Source: WHATWG: Barred from constraint validation
- willValidate: "A boolean indicating whether the element is a candidate for constraint validation." — Source: WHATWG: willValidate
Mental model: helpful checkpoint, unlocked gate
Native HTML validation works like an early checkpoint: it catches common mistakes and gives the user immediate feedback. It is not a locked security boundary. A user or a program can remove the attributes, turn off the browser behavior, or send a handcrafted HTTP request. The server therefore has to distrust and validate every value itself.
Choose a constraint based on what the data means:
requiredrejects an empty required value or a required choice that was not made.type="email"checks email-address syntax. It does not establish that the mailbox exists or belongs to the user.minlength/maxlengthconstrain the length of user-entered text on supporting text controls.min/maxconstrain numeric or temporal values; they do not constrain text length.patternapplies a regular expression to supported text-like inputs.
The presence of an attribute is not, by itself, a reason to validate. Tell users what you expect before submission, and leave room for realistic names, addresses, and international formats.
Guided example: validate registration
<form action="/register" method="post">
<p>All fields are required unless marked optional.</p>
<p>
<label for="full-name">Full name</label>
<input
type="text"
id="full-name"
name="full-name"
autocomplete="name"
required
minlength="2"
maxlength="100">
</p>
<p>
<label for="email">Email address</label>
<input
type="email"
id="email"
name="email"
autocomplete="email"
required
maxlength="254">
</p>
<p id="password-help">Use at least 12 characters. Long phrases are welcome.</p>
<p>
<label for="password">Create password</label>
<input
type="password"
id="password"
name="password"
autocomplete="new-password"
required
minlength="12"
maxlength="128"
aria-describedby="password-help">
</p>
<fieldset>
<legend>Preferred contact method</legend>
<input type="radio" id="method-email" name="contact-method" value="email" required>
<label for="method-email">Email</label>
<input type="radio" id="method-none" name="contact-method" value="none">
<label for="method-none">No contact</label>
</fieldset>
<p>
<label for="experience">Years of experience (optional)</label>
<input type="number" id="experience" name="experience" min="0" max="80" step="1">
</p>
<button type="submit">Create account</button>
</form>
The requirements are also visible in the form; they are not expressed only through attributes. aria-describedby is appropriate here because it connects the persistent password guidance to the input, but it does not replace the label. Giving one radio in a same-named group required means that the group needs a selected value. HTML only needs required on one group member, although applying it consistently is often easier for a team to maintain.
Test a matrix rather than trying one happy path:
| Control | Invalid test | Valid boundary |
|---|---|---|
| Name | empty, one character | two characters |
asha, empty | asha@example.com | |
| Password | eleven characters | twelve characters |
| Contact | none selected | either option |
| Experience | -1, 81, 1.5 | 0, 80, integer |
Submit each invalid state separately. The browser's messages and the date or number controls vary by browser and locale, so do not build a lesson or an application around exact wording. The useful observations are that focus moves to a failing control and that ordinary browser submission is blocked.
Validation switches and responsibility boundaries
Authors can deliberately bypass browser constraint validation when the form needs that behavior:
<form action="/register" method="post" novalidate>
...
<button type="submit">Submit without browser validation</button>
</form>
novalidate disables constraint validation for the form. A submit button can instead use formnovalidate, which bypasses validation for only that submission path. That can be useful for an action such as “Save draft.”
This is also a clear demonstration of the responsibility boundary. A malicious or custom client does not have to use your HTML form, which is why server-side validation remains mandatory.
Once JavaScript is introduced later, the Constraint Validation API provides validity, validationMessage, checkValidity(), reportValidity(), and setCustomValidity(). Custom messages can make errors clearer, but they do not remove the need for good labels and instructions, native constraints, or server checks.
Pattern without regex overreach
Suppose a project ID has one exact format: PROJ- followed by four ASCII digits.
<label for="project-code">Project code</label>
<p id="project-code-help">Format: PROJ-1234</p>
<input
type="text"
id="project-code"
name="project-code"
pattern="PROJ-[0-9]{4}"
aria-describedby="project-code-help">
HTML matches pattern against the entire value, so explicit ^ and $ anchors are unnecessary. Modern HTML specifies the pattern as a JavaScript regular expression compiled with the v flag, which affects escaping and character classes. Keep beginner patterns narrow, document the accepted format, and test it. A pattern does not make an optional empty control required; add required when empty is invalid.
Do not apply a simplistic pattern to human names or general email addresses. type="email" already supplies a browser syntax check, while the server still decides what the application accepts. An overly strict pattern can reject legitimate users.
Intermediate example: validated contact form
<form action="/contact" method="post">
<p>Required fields are marked “required.”</p>
<p>
<label for="contact-name">Name (required)</label>
<input type="text" id="contact-name" name="name" autocomplete="name" required maxlength="100">
</p>
<p>
<label for="contact-email">Email (required)</label>
<input type="email" id="contact-email" name="email" autocomplete="email" required maxlength="254">
</p>
<p>
<label for="contact-topic">Topic (required)</label>
<select id="contact-topic" name="topic" required>
<option value="">Choose a topic</option>
<option value="project">Project question</option>
<option value="feedback">Portfolio feedback</option>
<option value="other">Other</option>
</select>
</p>
<p id="message-help">Enter 20 to 1000 characters.</p>
<p>
<label for="contact-message">Message (required)</label>
<textarea id="contact-message" name="message" rows="8" cols="50" required minlength="20" maxlength="1000" aria-describedby="message-help"></textarea>
</p>
<button type="submit">Send message</button>
</form>
The first option has an empty value, so the required select stays invalid until the user chooses a real topic. It serves as an instruction, not as a replacement for the visible label. A textarea supports length constraints, but it does not support pattern.
Try a message containing only spaces. Native required considers spaces non-empty, which exposes one of its limits. The server should trim or apply the application's business rules and return accessible errors without throwing away other valid entries.
Advanced optional extension: validation APIs and bypass
JavaScript can use checkValidity(), reportValidity(), validity, and setCustomValidity() to work with the browser's validation state. Do not add JavaScript during this HTML phase; the point here is to recognize what later enhancement can do. Custom errors must be cleared once the value becomes valid and must be associated with and announced accessibly. That takes more work than relying on native validation.
For a controlled local test, add novalidate temporarily and submit invalid values. Browser blocking stops, which proves that attributes are not security controls. Remove the attribute. Then send a modified request in that controlled environment. Whether the browser ran checks must never determine whether the server accepts the request.
Common mistakes and debugging
min/maxused for text length: useminlength/maxlength.minlengthassumed to imply required: optional empty values can still be valid.- Pattern without visible format: add concise instructions.
- Regex used for names/email unnecessarily: accept realistic international input.
- Placeholder-only requirements: keep instructions visible.
- Relying on color or browser bubble alone: identify requirements and plan persistent server errors.
- Spaces accepted as message: apply authoritative server business rules.
- Client validation called security: demonstrate bypass with
novalidate. - Conformance checker confused with form validation: use both for different error classes.
Accessibility, security, and performance
WCAG requires labels and instructions, along with text-based identification of errors. Native validation differs across browsers and may not cover every error-recovery need, particularly after the server rejects a submission. Preserve the user's entered values, identify each problem in text, connect errors to their controls, and suggest a fix when one is known. Do not disable paste in password controls; password managers and accessible authentication workflows depend on it.
On the server, validate, normalize carefully, authorize, and encode. Length limits help control resource use, but they are not a complete denial-of-service defense. Never log plaintext passwords. Native constraints are inexpensive and can avoid shipping a validation library for basic rules, but the server round trip still exists and needs to be handled efficiently.
Tiered exercises
Level 1: match constraints
Choose constraints for a required email, an optional integer from 0 through 10, a required message of 20-500 characters, and an optional PROJ-1234 code.
Level 2: apply and test
Add constraints to the registration form. Create invalid, boundary-valid, and ordinary-valid cases for every control.
Level 3: limitations
Validate the contact form, test whitespace and novalidate, run the Nu checker, and explain what response the server should return for invalid data.
Level 1: email uses type="email" required; integer uses type="number" min="0" max="10" step="1"; message uses required minlength="20" maxlength="500"; code uses pattern="PROJ-[0-9]{4}" together with visible instructions, without required.
Level 2: use the guided form and its matrix. Boundaries include 2/100 characters for the name, 12/128 for the password, and 0/80 for experience. Test one value just inside and just outside each boundary. Exact browser messages can differ; the expected validity result cannot.
Level 3: the intermediate form is complete. Whitespace can satisfy required, and novalidate bypasses browser blocking. Nu checks markup conformance, not business correctness. The server revalidates every field, rejects invalid entries, preserves safe input, and returns specific text errors associated with the relevant controls.
Recap and exit questions
Native constraints give users faster feedback and reduce accidental bad submissions. They need to match the data, be communicated visibly, be tested at boundaries, and be repeated authoritatively on the server.
- Why does
type="email"not verify mailbox ownership? - How do length and range attributes differ?
- Does
patternmake a field required? - What does
novalidatedemonstrate? - How does conformance checking differ from constraint validation?
Try it with your own example
Constraints become much easier to reason about after you watch the browser block a bad submission you entered yourself. Add them to your own cake-order form from lesson 009.
Rina says pickup requests need at least three days' notice on the size choice, and the customer must provide a name so staff can find the order:
<form action="/order-cake" method="post">
<p>
<label for="customer-name">Your name</label>
<input type="text" id="customer-name" name="customer-name" required minlength="2" maxlength="80">
</p>
<fieldset>
<legend>Cake size</legend>
<input type="radio" id="size-6" name="size" value="6-inch" required>
<label for="size-6">6-inch (serves 6–8)</label>
<input type="radio" id="size-8" name="size" value="8-inch">
<label for="size-8">8-inch (serves 10–12)</label>
</fieldset>
<button type="submit">Send order request</button>
</form>
Submit it with the name field empty. The browser should refuse to send the request and move focus to that field. That is native constraint validation working before you have written any JavaScript. Now open DevTools, temporarily add novalidate to the <form> tag, and submit the empty form again. It goes through. The experiment captures the lesson's central boundary: the attribute you removed was a courtesy to the browser, not a security control, and Rina's real order-processing server must reject an empty name every time, regardless of what any browser did first.
Further reading: MDN — Client-side form validation has a live example you can edit in-browser to see other validity states like rangeOverflow fire.
