Lesson 9 of 32
Form Validation
Built-in HTML validation — required, pattern, min/max, length limits — styling with :valid/:invalid, grouping with fieldset, and why client-side validation is UX, not security.
The previous lesson covered collecting input. This one covers making sure what's collected is actually usable, using validation features built directly into HTML — no JavaScript required for the basics.
required
<label for="email">Email</label>
<input type="email" id="email" name="email" required />A required field blocks form submission (and shows a browser-native message) if left empty. Simple, and works on virtually every input type.
pattern — matching a regular expression
<label for="zip">ZIP code</label>
<input type="text" id="zip" name="zip" pattern="[0-9]{5}" title="Five digits" />pattern takes a regular expression the value must fully match. Here,
[0-9]{5} requires exactly five digits. The title attribute is worth
including alongside pattern — many browsers show it as part of the
validation message, explaining what format is actually expected.
Length and range limits
<input type="text" minlength="3" maxlength="20" />
<input type="number" min="1" max="100" />
<input type="date" min="2026-01-01" max="2026-12-31" />minlength/maxlength constrain text length. min/max work on numeric
and date-like inputs, constraining the value's range rather than its
character count.
Styling based on validation state: :valid and :invalid
CSS can style an input differently depending on whether its current value passes validation, using two pseudo-classes:
input:invalid {
border-color: #dc2626;
}
input:valid {
border-color: #16a34a;
}A plain <input> with no constraints at all is considered :valid by
default, since there's nothing to fail. Once you add required, pattern,
or similar, the browser evaluates the current value against it live, and
:invalid matches the moment it doesn't satisfy them. A common refinement is
only showing red for a field the visitor has actually interacted with,
combining :invalid with :not(:placeholder-shown) or similar — worth
knowing exists, though the full selector toolkit is a later lesson's
territory.
<fieldset> and <legend>
<fieldset>
<legend>Shipping preference</legend>
<label><input type="radio" name="shipping" value="standard" /> Standard</label>
<label><input type="radio" name="shipping" value="express" /> Express</label>
</fieldset><fieldset> groups related controls — most commonly a set of radio buttons
or checkboxes that belong together as one question. <legend> gives that
group a label, and it's genuinely announced by screen readers as the group's
purpose when a control inside it receives focus — so a visitor tabbing into
one of these radios hears "Shipping preference: Standard" rather than just
"Standard" with no context for what it's a choice between.
novalidate — opting out of built-in validation UI
<form novalidate>
...
</form>Adding novalidate to a <form> turns off the browser's automatic
validation blocking and default error messages entirely, even if the inputs
still have required, pattern, and so on. This is useful when you want to
build fully custom validation messaging and behaviour (typically with
JavaScript) instead of the browser's default styling, while still keeping
the validation attributes present in the markup as a record of the rules.
Without a JavaScript replacement in place, though, turning it off just
removes protection with nothing in its place — so this attribute makes sense
alongside custom validation logic, not as a standalone change.
Client-side validation is UX, not security
This is worth stating plainly: everything in this lesson happens in the
visitor's browser, which means it can be bypassed entirely. A visitor (or
an attacker) can send a request directly to your server's endpoint using
tools that skip your HTML form completely — curl, a script, a browser's
developer tools, anything that can make an HTTP request. None of your
required, pattern, or min/max attributes are consulted at all in that
path, because the browser's form was never involved.
That means the server must always independently re-validate every piece of data it receives, regardless of what the client-side form enforced. Client-side validation exists purely to make the experience better for a real visitor filling out a real form — instant feedback, no wasted round trip to the server for an obvious mistake like a missing required field. It is not, and cannot be, a substitute for validating on the server.
Try it yourself
What to remember
required,pattern,minlength/maxlength, andmin/maxare built-in browser validation, no JavaScript needed.:validand:invalidlet you style a field based on whether its current value satisfies those constraints.<fieldset>+<legend>groups related controls with an accessible shared label.novalidateturns off the browser's default validation UI, intended for pairing with custom validation logic — not as a standalone removal.- Client-side validation is a UX convenience only. A request can bypass the browser entirely, so the server must always re-validate.
Check yourself
3 questions · pass 3/3 to unlock Accessibility Fundamentals
1.Why is client-side HTML validation not a security measure?
2.What does the
patternattribute on an<input>accept?3.What is
<fieldset>with<legend>used for?
3 left to answer