AniUI Academy

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.

8 min read

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

Try it yourself
Loading playground...

What to remember

  • required, pattern, minlength/maxlength, and min/max are built-in browser validation, no JavaScript needed.
  • :valid and :invalid let you style a field based on whether its current value satisfies those constraints.
  • <fieldset> + <legend> groups related controls with an accessible shared label.
  • novalidate turns 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

up to 50
  1. 1.Why is client-side HTML validation not a security measure?

  2. 2.What does the pattern attribute on an <input> accept?

  3. 3.What is <fieldset> with <legend> used for?

3 left to answer