Built In Form Validation
The browser can check most of a form before it is sent, with no JavaScript. Learn the attributes, the CSS hooks, and why none of it replaces server validation.
-
HTML Basics
- What is HTML: The Structure Layer of Every Web Page
- HTML Document Structure: DOCTYPE, html, head and body
- Elements, Tags and Attributes: The Vocabulary of HTML
- HTML Comments: Notes That Ship With Your Code
- Block Level and Inline Elements
- Writing and Running Your First HTML Page
- How a Browser Turns Markup Into a Page
- Text and Formatting
- Links and Navigation
- Images and Media
- Lists
- Tables
-
Forms
- Form Structure: form, action and method
- Input Types: Text, Email, Number, Date and the Rest
- Labels: The Most Important Element in a Form
- Checkboxes, Radio Buttons and Grouping
- select, option, optgroup and datalist
- textarea, File Uploads and Hidden Fields
- Buttons: submit, reset and button
- Built In Form Validation
- GET or POST: What Happens When a Form Is Submitted
- Semantic HTML
- HTML5 Features
- Head and Metadata
- HTML with CSS
- HTML with JavaScript
- Accessibility
-
HTML SEO
- How Google Works: Crawling, Indexing and Ranking
- SEO Friendly HTML Structure
- Titles and Descriptions That Earn Clicks
- Headings and Content Structure for Search
- Internal Linking and Anchor Text
- robots.txt and XML Sitemaps
- Canonical URLs and Duplicate Content
- Structured Data and JSON-LD
- Image SEO
- Core Web Vitals and Mobile Friendliness
- DevTools and Debugging
- Editor Productivity
- HTML Best Practices
- HTML Projects
- Advanced Projects
- Practice and Exams
Concept
HTML can validate a form on its own. Add a few attributes and the browser refuses to submit an invalid form, focuses the first problem field and shows a message - all before any script runs.
It is a convenience for the reader, not a security measure. Every attribute here can be removed in the developer tools in five seconds, and a request can be sent without a browser at all. The server must validate everything again, without exception.
The attributes
| Attribute | Checks | Applies to |
|---|---|---|
required | Not empty | Most controls |
type | Format for email, url, number, dates | input |
minlength / maxlength | Character count | Text controls |
min / max | Value range | Number, range, date, time |
step | Allowed increments | Number, range, date, time |
pattern | A regular expression | Text style inputs |
Example
<form action="/register" method="post">
<p>
<label for="user">Username</label>
<input type="text" id="user" name="user"
required minlength="3" maxlength="20"
pattern="[a-z0-9_]+"
aria-describedby="user-help">
<small id="user-help">Three to twenty characters: lower case letters, digits and underscores.</small>
</p>
<p>
<label for="email">Email address</label>
<input type="email" id="email" name="email" required autocomplete="email">
</p>
<p>
<label for="age">Age</label>
<input type="number" id="age" name="age" min="16" max="120" step="1" required>
</p>
<p>
<label for="start">Preferred start date</label>
<input type="date" id="start" name="start" min="2026-09-01" max="2027-08-31">
</p>
<button type="submit">Create account</button>
</form>pattern
pattern takes a JavaScript regular expression, anchored automatically at both ends - the whole value must match, so there is no need to write the start and end anchors.
<input pattern="[0-9]{6}"> <!-- exactly six digits -->
<input pattern="[A-Z]{5}[0-9]{4}[A-Z]"> <!-- a fixed code format -->
<input pattern="[a-z0-9._%+-]+@[a-z0-9.-]+.[a-z]{2,}">
<input pattern="(+91)?[6-9][0-9]{9}"> <!-- optional country code -->Two rules that make pattern usable rather than infuriating:
- Always pair it with a visible hint describing the format. A reader who cannot see the pattern cannot guess it.
- Always add a
title, which browsers append to the error message.
<input type="text" name="pin"
pattern="[0-9]{6}"
title="Six digits"
aria-describedby="pin-help">
<small id="pin-help">Six digits, for example 411001</small>Resist the temptation to write a strict email pattern. Real addresses are far stranger than most regular expressions allow, and type="email" already applies a sensible check.
Styling validity
input:invalid { border-color: #dc2626; }
input:valid { border-color: #16a34a; }
input:required { border-left: 3px solid #1e3a8a; }Applied naively this is hostile: an empty required field is invalid from the moment the page loads, so the whole form is outlined in red before the reader has typed anything.
:user-invalid fixes it. It matches only after the reader has interacted with the field or attempted to submit:
input:user-invalid {
border-color: #dc2626;
background: #fef2f2;
}
input:user-invalid + .error-message { display: block; }Custom messages
The default messages are localised by the browser but generic. setCustomValidity replaces them:
const pin = document.getElementById("pin");
pin.addEventListener("input", () => {
if (pin.validity.patternMismatch) {
pin.setCustomValidity("A PIN code is exactly six digits.");
} else {
pin.setCustomValidity(""); // an empty string clears it
}
});Forgetting to clear it is a classic bug: the field stays permanently invalid because a stale custom message is still set.
The validity object
input.validity.valueMissing // required but empty
input.validity.typeMismatch // wrong format for the type
input.validity.patternMismatch // failed the pattern
input.validity.tooShort // below minlength
input.validity.tooLong // above maxlength
input.validity.rangeUnderflow // below min
input.validity.rangeOverflow // above max
input.validity.stepMismatch // not on a step boundary
input.validity.valid // passes everythingTurning it off
<form novalidate>...</form>
<button type="submit" formnovalidate>Save draft</button>Adding novalidate and validating in script is a legitimate choice - it gives full control over the messages and their placement, which the native bubbles do not. Keep the attributes on the fields regardless, so the validity API still works.
Accessible error messages
The native bubble disappears quickly and is not always announced. For a form that matters, show errors in the page:
<p>
<label for="email">Email address</label>
<input type="email" id="email" name="email" required
aria-describedby="email-error" aria-invalid="true">
<span id="email-error" class="error" role="alert">
Enter an email address, for example meera@example.com
</span>
</p>aria-invalid="true"marks the field as failing.aria-describedbyties the message to the field, so it is announced on focus.role="alert"announces it immediately when it appears.- Never signal an error with colour alone. Use text and, ideally, an icon.
Important rules
- Client side validation is never a security boundary.
patternis anchored at both ends automatically.requiredon one radio applies to the whole group.minandmaxon a date field take ISO format values.- A hidden or disabled field is not validated.
- Only the first invalid field is focused and reported.
Common mistakes
- Trusting the browser and skipping server validation.
- Styling
:invalidinstead of:user-invalid, painting an untouched form red. - A
patternwith no visible hint. - Over strict email patterns rejecting valid addresses.
- Forgetting to clear a custom validity message.
- Relying on the native bubble as the only error indication.
- Colour only error states.
Best practices
- Use the native attributes first; add script only for what they cannot express.
- Validate on the server for the same rules, always.
- Show the format requirement before the reader types, not after they fail.
- Use
:user-invalidfor styling. - Put errors next to the field, in text, tied with
aria-describedby. - Summarise errors at the top of a long form and link each to its field.
- Never clear a form because one field failed.
Practice
- Build a registration form using only native validation and attempt to submit it empty.
- Style with
:invalidfirst, then switch to:user-invalidand compare the first impression. - Add a
patternwith atitleand a visible hint, and check the error message. - Bypass the validation in DevTools and submit an invalid value. Explain what the server must do.