HTML form validation comes in a few distinct flavours, and each one has a job the others can't do. Browser-native validation (using attributes like
required, type="email", and pattern) catches obvious mistakes instantly, JavaScript validation handles rules the browser doesn't understand, and server-side validation is the only layer you can actually trust because it's the only one a user can't bypass. Most solid forms use all three, layered, with inline feedback on top to tell people what went wrong while they're still looking at the field.
Content Table
- The four types of form validation at a glance
- Client-side validation with built-in HTML attributes
- JavaScript validation for rules HTML can't express
- Server-side validation and why it is never optional
- Inline validation vs on-submit validation
- When each validation type applies
- Writing validation feedback people can act on
The four types of form validation at a glance
People usually say there are two types of form validation, client-side and server-side. That's true at the architecture level, but in practice you're making four separate decisions: where the check runs, who wrote it, when it fires, and how the message appears.
| Type | Runs where | Good at | Can be bypassed? |
|---|---|---|---|
| Native HTML validation | Browser, before submit | Empty fields, basic formats, min/max, length limits | Yes (DevTools, curl, JS off) |
| JavaScript / custom client-side validation | Browser, on input or submit | Cross-field rules, custom messages, async checks | Yes |
| Server-side validation | Your backend or form service | Security, data integrity, business rules, uniqueness | No |
| Inline validation (feedback layer) | Browser UI | Telling the user what's wrong, field by field | N/A (it's presentation) |
Client-side validation with built-in HTML attributes
This is the cheapest validation you will ever write, because you barely write it. Add attributes to your inputs and the browser enforces them before the form submits, in the user's own language, with zero JavaScript.
<form action="/subscribe" method="POST">
<label for="email">Email</label>
<input id="email" type="email" name="email" required maxlength="255">
<label for="age">Age</label>
<input id="age" type="number" name="age" min="18" max="120" step="1">
<label for="zip">ZIP code</label>
<input id="zip" name="zip" pattern="[0-9]{5}" title="Five digits, e.g. 90210">
<button type="submit">Subscribe</button>
</form>
The attributes that do the heavy lifting:
-
requiredblocks empty submission. It works differently on checkboxes, radio groups, and hidden fields, which is where most bugs come from (there's a longer breakdown of when the required attribute actually works). -
typegives you free format checks:email,url,number,date,tel(note thattelenforces nothing, it only changes the mobile keyboard). -
min,max,stepfor numbers and dates. -
minlengthandmaxlengthfor text length. -
patternfor anything regex-shaped, like postal codes or invoice numbers. See the guide to using the pattern attribute for input validation for the gotchas around anchoring and Unicode.
fetch()
or form.submit(), the browser skips constraint checking entirely. Call
form.reportValidity()
yourself first.
Two structural limits worth knowing:
<textarea>
ignores
pattern
completely, so length limits and JS are your only tools there (more on that in these
HTML textarea best practices
). And a free-text input paired with a
<datalist>
accepts values that aren't in the list, which is exactly the difference explained in
datalist vs select. A
<select>
with
required
and an empty first option is the only version that guarantees a value from your list.
JavaScript validation for rules HTML can't express
HTML attributes can't compare two fields, look something up, or say "this password needs a symbol, and here's a friendly reason why". That's the job of the Constraint Validation API , which lets you read the browser's own verdict and add your own on top.
const pw = document.querySelector('#password');
const confirm = document.querySelector('#confirm');
function checkMatch() {
confirm.setCustomValidity(
confirm.value === pw.value ? '' : 'Passwords do not match.'
);
}
confirm.addEventListener('input', checkMatch);
pw.addEventListener('input', checkMatch);
Reach for client-side JavaScript validation when you need:
- Cross-field rules: end date after start date, password confirmation, "at least one contact method".
- Custom wording: "Use your work email" reads better than the browser's generic message.
- Async checks: is this username taken, is this coupon still valid.
- Conditional requirements: a field that only becomes required after a checkbox is ticked.
- Progressive step gating in a multi-step form , where you validate each step before advancing.
Use
ValidityState
properties (valueMissing, typeMismatch, patternMismatch, tooShort, rangeOverflow) rather than rewriting format checks from scratch. The browser already knows what's wrong; you just need to phrase it better.
Server-side validation and why it is never optional
Everything above runs on a machine you don't control. A browser extension, a proxy, DevTools, or a two-line curl command can submit any payload it likes straight to your endpoint. So client-side validation is a user experience feature. Server-side validation is the actual rule.
Your server needs to re-check everything the client checked, plus the things the client couldn't:
-
Re-verify format and length.
Never trust that
emailis an email or that a 3000-character limit was respected. -
Type and range coercion.
A number field can arrive as
"abc","1e9", or an array. - Uniqueness and existence. Duplicate emails, valid product IDs, real order numbers.
- Authorisation. Is this user even allowed to edit this record? No client check can answer that.
- Sanitising and escaping before storage or display, following the OWASP input validation guidance.
- File checks. Actual MIME type and real byte size, not the filename extension. And the request has to be encoded correctly in the first place, which is where the form enctype attribute matters for uploads.
- Abuse controls. Rate limits, spam scoring, honeypot handling, CSRF tokens.
Inline validation vs on-submit validation
Same rules, different moment. Timing changes how the form feels more than almost anything else.
| Approach | Fires | Best for | Risk |
|---|---|---|---|
| Inline on blur | When the user leaves a field | Most fields: email, phone, postal code | Feels harsh if the user only tabbed through |
| Inline live (on input) | Every keystroke | Password strength, character counters, username availability | Yelling "invalid email" after one letter is typed |
| On submit | When the user clicks send | Cross-field rules, short forms, final safety net | User fixes errors one round-trip at a time |
| After server response | After the request completes | Duplicate records, expired codes, payment failures | Losing entered data if you don't repopulate the form |
The pattern that annoys the fewest people: validate on blur for the first pass, then switch that field to live validation once it has an error, so the message clears the moment they fix it. Never validate a field before the user has touched it.
When each validation type applies
Concrete mapping, field by field:
- Simple contact form (name, email, message): native HTML attributes plus server-side re-validation. JavaScript is optional here.
- Signup with password confirmation: native for format, JavaScript for the match check and strength meter, server-side for uniqueness and hashing rules.
-
Checkout address:
native
patternfor postal codes, JavaScript to swap the pattern when the country changes, server-side address verification. -
File upload:
acceptand a client size check for fast feedback, server-side type and size enforcement as the real gate. -
Date range booking:
native
min/max, JavaScript for "checkout after checkin", server-side for real availability. - Anything touching money, accounts, or other people's data: full stack of all three, no exceptions.
One more layer that isn't really validation but sits next to it: bot filtering. Format rules won't stop automated submissions, which is why honeypot fields and server-side rate limiting exist alongside your field checks.
Writing validation feedback people can act on
An error message has one job: tell the user exactly what to change. "Invalid input" fails that test. Specific beats polite.
- Name the field and the fix: "Phone number needs 10 digits, no spaces" instead of "Please enter a valid phone number".
- Put the message next to the field, not only in a banner at the top.
-
Use
aria-describedbyto link the input to its error text, andaria-invalid="true"when it fails, so screen readers announce it. This is what WCAG 3.3.1 Error Identification asks for. - Don't rely on red alone. Colour plus icon plus text.
- Move focus to the first invalid field after a failed submit, and keep everything the user already typed.
- Show a success state sparingly. Green ticks on every field become noise; they help most on fields with strict rules like passwords or usernames.
Browser default bubbles are fine for a prototype, but they disappear on scroll and can't be styled. If you want consistent, accessible messaging, suppress them with
novalidate
on the form, keep the attributes for their validity state, and render your own messages from the
constraint validation API.
Get server-side validation without running a server
Native HTML form validation is easy, but the server-side half needs a backend you may not have. SendForm's serverless contact form validates and stores each submission, filters spam, and emails it to you, so a static site still gets the layer users can't bypass.
Create a contact form →
No. Anyone can delete the
required
attribute in DevTools or post directly to your endpoint with curl, skipping every browser check. Client-side validation improves speed and clarity, but the receiving side still has to validate format, length, and content before storing or emailing anything.
Three usual causes: the form has the
novalidate
attribute, the submit button carries
formnovalidate, or you're submitting through JavaScript with
fetch() or form.submit(), which bypasses constraint checking. In the JavaScript case, call
form.reportValidity()
before sending the request.
Validate on blur for the first check, since flagging an email as invalid after one typed character is frustrating. Once a field is marked invalid, switch it to live validation on input so the error disappears the instant the user corrects it. Keep live-only validation for password strength and character counters.
No. It only checks the shape of the string, roughly "something@something".
a@b
passes. Real verification needs a server-side check, usually a domain lookup or a confirmation link sent to the address. Treat
type="email"
as typo prevention, not proof of a working mailbox.
Set
aria-invalid="true"
on the failing input and point
aria-describedby
at the element holding the error text. Keep the message as real text, not just a colour or icon, move focus to the first invalid field after a failed submit, and make sure every input has a properly associated label.
Never on their own. A
pattern
attribute runs in the browser and can be stripped out in seconds. Mirror the same rule on the server, and keep patterns simple: overly clever regex for emails or phone numbers rejects valid real-world values more often than it blocks bad ones.
