010: HTML Validation
Learning outcomes
By the end of this lesson, you can apply required, length, range, type, and pattern constraints; test native browser validation systematically; distinguish constraint validation from markup conformance; and explain why all client-side checks must be repeated on the server.
Prerequisites and retrieval
Use 009's complete form. Identify each control's name and value and explain why users can modify even predefined values before a request reaches the server. Recall that the Nu checker finds markup mistakes; today's browser constraint validation checks entered values.
Terminology
- Constraint: A validation rule attached to a control through attributes such as required, minlength, pattern. — Source: WHATWG: Constraints
- Constraint validation: The browser mechanism checking whether controls satisfy their applicable constraints. — Source: WHATWG: Constraint validation API
- Valid/invalid: Whether a control currently suffers no validity-state failures (validity.valid). — Source: WHATWG: ValidityState
- Type mismatch: A validity failure where the entered value does not match the type’s required syntax (email/url). — Source: WHATWG: Type mismatch
- Pattern: The pattern attribute compiles a JavaScript regular expression matched against the entire value. — Source: WHATWG: pattern attribute
- Range: min, max, and step bounds constraining numeric and temporal input values. — Source: WHATWG: min/max attributes
- Client-side validation: Constraint checks performed in the browser before submission completes. — Source: MDN: Client-side form validation
- Server-side validation: Authoritative checks after receipt; client-side checks are convenience, not security. — Source: MDN: Client-side form validation
- Conformance validation: Checking markup against authoring rules with a checker — separate from validating 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 is a checkpoint that catches mistakes early and gives immediate feedback. It is not a locked security boundary. A user or program can remove attributes, disable browser behavior, or send a handcrafted HTTP request. The server must distrust and validate every value.
Constraint choice follows data meaning:
requiredrejects an empty required value or missing required choice.type="email"checks an email-address syntax, not whether the mailbox exists or belongs to the user.minlength/maxlengthconstrain user-entered text length on supporting text controls.min/maxconstrain numeric or temporal values, not text length.patternconstrains supported text-like inputs with a regular expression.
Do not validate merely because an attribute exists. Communicate requirements before submission, and accept 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>
Requirements are visible, not represented only by attributes. aria-describedby is justified because it associates persistent password guidance with the input; it does not replace the label. Requiring one radio in a same-named group means one group value must be selected. Putting required on one member is sufficient by HTML behavior, though teams often apply it consistently for maintainability.
Test a matrix:
| 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. Browser messages and date/number UI vary by locale and browser. Do not promise exact wording. Focus should move to a failing control and submission should be blocked in ordinary browser use.
Validation switches and responsibility boundaries
HTML lets authors intentionally bypass browser constraint validation:
<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 also use formnovalidate to bypass validation for only that submission path, which can be useful for actions such as “Save draft.”
These attributes are another reason server-side validation is mandatory. A malicious or custom client does not need to use your HTML form at all.
When JavaScript is introduced later, the Constraint Validation API exposes properties such as validity, validationMessage, checkValidity(), reportValidity(), and setCustomValidity(). Custom messages can improve clarity, but they do not replace correct labels, instructions, native constraints, or server checks.
Pattern without regex overreach
If a project ID must be exactly PROJ- plus 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 pattern matching applies to the entire value; explicit ^ and $ anchors are unnecessary. Modern HTML specifies the pattern as a JavaScript regular expression compiled with the v flag, affecting escaping and character classes. Keep beginner patterns narrow, document accepted input, and test. Pattern does not make an optional empty control required; add required if empty is invalid.
Do not use a simplistic pattern for human names or general email addresses. type="email" already provides a browser syntax check, and server policy still decides what is accepted. Overly strict patterns exclude 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's empty value makes the required select invalid until a real topic is chosen. It remains an instruction, not a substitute for the visible label. textarea supports length constraints but not pattern.
Test whitespace-only messages. Native required treats spaces as a non-empty value, demonstrating a limitation. The server should trim or apply product-specific rules and return accessible errors without discarding other valid entries.
Advanced optional extension: validation APIs and bypass
The browser exposes checkValidity(), reportValidity(), validity, and setCustomValidity() to JavaScript. Do not add JavaScript in this HTML phase; understand that enhancement exists. Custom errors must be cleared when valid and associated/announced accessibly, which is more work than native validation.
Add novalidate temporarily to the form and submit invalid values. Browser blocking is disabled, proving that attributes are not security. Remove it. Next send a modified request in a controlled local environment. Server acceptance must never depend on the browser having run checks.
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/instructions and text identification of errors. Native validation varies and may not meet every error-recovery need by itself, especially after server rejection. Preserve entered values, identify each problem in text, link errors to controls, and suggest fixes where known. Do not disable paste in password controls; password managers and accessible authentication benefit from it.
Validate, normalize carefully, authorize, and encode on the server. Length limits help resource control but are not a complete denial-of-service defense. Never log plaintext passwords. Native constraints cost little and avoid shipping a validation library for basic rules, but server round trips still exist and must be efficient.
Tiered exercises
Level 1: match constraints
Choose constraints for required email, optional integer 0-10, required 20-500 character message, and optional PROJ-1234 code.
Level 2: apply and test
Add constraints to registration. Create invalid, boundary-valid, and ordinary-valid cases for each control.
Level 3: limitations
Validate the contact form, test whitespace and novalidate, run the Nu checker, and explain the server response required 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}" plus visible instructions, without required.
Level 2: use the guided form and matrix. Boundaries include 2/100 name characters, 12/128 password characters, and experience 0/80. Test just outside each boundary. Exact browser messages may differ; expected validity does not.
Level 3: the intermediate form is complete. Whitespace can pass required, and novalidate bypasses 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 controls.
Recap and exit questions
Native constraints improve feedback and reduce accidental bad submissions. They must 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 only feel real once you've watched the browser actually block a bad submission you typed yourself, so add them to your own cake-order form from lesson 009 right now.
Rina tells you pickup requests need at least three days' notice on the size choice, and the customer must give 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. Your browser should refuse to send the request and move focus to that field — that is native constraint validation doing its job, for free, before you've written a line of JavaScript. Now open DevTools, temporarily add novalidate to the <form> tag, and submit the empty form again. It goes through. That single experiment is the whole point of this lesson: the attribute you just removed was a courtesy to the browser, never a security boundary, and Rina's real order-processing server must reject an empty name on its own, 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.
