What does the browser check before it submits?
Every form control has a validity state the browser keeps up to date as the user types. When the form is submitted, the browser checks each control in order. If any fails, submission stops, focus moves to the first invalid field, and the browser shows a message bubble in the user's language. No request is sent.
The checks come from attributes you write: required, minlength, maxlength, min, max, step, pattern, and the type itself for email and URL. You can read the result in the Console with input.validity, which returns flags such as valueMissing, typeMismatch, patternMismatch and rangeUnderflow. input.validationMessage holds the text the bubble will show.
Client-side checks are for the user's convenience. Anyone can remove the attributes in DevTools or send a request with curl, so the server must repeat every rule it depends on.
<form action="/join" method="post">
<label for="em">Email</label>
<input id="em" name="email" type="email" required>
<button>Join the bird club</button>
</form>

required and type="email". An invalid value stops the submit and puts focus on the field. Your server still has to check again, because this step can be skipped.How does required behave on each control?
On text fields, required fails when the value is empty. A single space counts as a value, so trim on the server. On a checkbox, it means the box must be ticked, which suits I accept the terms. On a radio group, putting required on any one radio makes the whole group required. On a select, it fails when the selected option has an empty value, which is why required selects start with a placeholder option.
The required attribute is announced by screen readers as required, so you do not need aria-required as well. Sighted users still need a visible cue. Write required or optional in the label text instead of relying on a red asterisk alone, and explain the convention at the top of the form if you use one.
Disabled and readonly fields are skipped by validation entirely. A readonly field marked required never blocks submission.
<label for="name">Full name (required)</label>
<input id="name" name="name" required autocomplete="name">How do length, range and step limits work?
The minlength and maxlength attributes count characters in text fields and textareas. maxlength stops typing at the limit, so users rarely see an error for it. minlength only fails after the user has edited the field and only when the value is non-empty, so pair it with required if the field must be filled.
The min, max and step attributes apply to number, range and date and time types. Out-of-range values set rangeUnderflow or rangeOverflow. Values that miss the step set stepMismatch, and the browser message suggests the two nearest valid values. A number field with step="5" and min="0" rejects 12 and suggests 10 or 15.
Lengths count UTF-16 code units, so an emoji can use two characters of a maxlength. Server limits in bytes may differ again. Keep the HTML limit equal to or smaller than the database limit so the browser catches it first.
<input name="tickets" type="number" min="1" max="6" step="1" required>
<textarea name="bio" minlength="20" maxlength="300"></textarea>How should you write a pattern?
The pattern attribute takes a regular expression that must match the whole value. The browser wraps it in ^(?: and )$ for you, so anchors are unnecessary. Since 2023 browsers compile it with the v flag, which makes some characters such as - inside character classes need escaping. A pattern that throws an error is ignored silently, so test it in the Console first.
Put a description of the format in the label or in hint text connected with aria-describedby. The title attribute is added to some browser messages, but it is unreliable on touch devices and is no substitute for visible guidance.
Patterns suit fixed formats you control: an order code like B-2041, a UK sort code, a membership number. They do not suit names, addresses or email addresses. A pattern that bans apostrophes rejects O'Brien, and one that bans + rejects valid emails. When in doubt, accept the input and check on the server.
<label for="ref">Order reference, like B-2041</label>
<input id="ref" name="ref" pattern="B-[0-9]{4}" required aria-describedby="ref-hint">
<p id="ref-hint">Find it in your confirmation email.</p>How do you style errors without shouting at users?
The :invalid pseudo-class matches a field the moment the page loads, before the user has touched it. Styling it red makes an empty form look broken. Use :user-invalid and :user-valid instead. They only match after the user has changed the field or tried to submit, and are supported in all current major browsers.
Colour alone is not enough. Pair a red border with an icon and a text message, and keep contrast at 3:1 for the border and 4.5:1 for the text. Put the message directly under the field and connect it with aria-describedby so screen readers read it with the field.
The :required and :optional pseudo-classes let you mark fields without extra classes, and :in-range and :out-of-range work on number and date fields.
input:user-invalid {
border-color: #b3261e;
outline: 2px solid #b3261e;
}
input:user-invalid + .error { display: block; }When should you turn off browser messages with novalidate?
The native bubbles disappear after a few seconds, cannot be styled, show only one error at a time and are not always announced by screen readers. For a short form they are fine. For a long checkout or an application form, write your own messages.
Add novalidate to the <form> to stop the browser blocking submission. Keep the attributes on the fields, because the constraint validation API still works. On submit, call form.checkValidity(), loop over the controls, read each validity and show your own message beside each failing field. Then move focus to an error summary at the top that links to each field.
Use setCustomValidity('message') for rules HTML cannot express, such as Confirm email must match. Call it with an empty string when the value is fixed, or the field stays invalid forever. A formnovalidate attribute on one submit button, such as Save draft, skips validation for that button only.
<form action="/apply" method="post" novalidate>
...
<button>Submit application</button>
<button formnovalidate name="intent" value="draft">Save draft</button>
</form>Where do output, progress and meter fit?
These three elements show values rather than collect them. The <output> element holds the result of a calculation, such as a running total for a cake order. Its for attribute lists the ids of the inputs it depends on, and many screen readers announce changes to it as a live region.
The <progress> element shows how far a task has gone: an upload at 60 percent, step 2 of 4 in a sign-up. Leave out value for an indeterminate bar while you wait for a server. The <meter> element shows a measurement within a known range, like password strength or storage used, and its low, high and optimum attributes change its colour.
Give each one a label or visible text. A bare progress bar tells a screen reader user nothing about what is loading.
<label for="up">Uploading photo</label>
<progress id="up" value="60" max="100">60%</progress>
<output name="total" for="qty size">24.00</output>What does AI-generated validation usually get wrong?
Validation code from AI assistants tends to look thorough while missing the basics. Check for these:
- Patterns with
^and$anchors, or patterns that break under thevflag rules. - Strict regexes on names, emails or phone numbers that reject real input.
minlengthwithoutrequired, which lets an empty field through.- Error styles on
:invalidthat show red borders on page load. - Custom scripts that call
setCustomValidity()and never clear it. - Error messages that sit in a separate div with no
aria-describedbylink to the field. - No mention of server validation. Ask for it explicitly, then test by submitting the form with the attributes removed in DevTools.
Field notes
- The browser checks every field before it sends a form and stops at the first problem.
- Add
requiredto a field that must not be left empty. - Limits such as
minlength,minandmaxset the allowed size or range of an answer.
Easy to get wrong
The browser already anchors pattern to the whole value. The extra anchors are harmless but noisy, and the inputmode helps phone users.
minlength does not reject an empty field. Add required if a value must be given.
meter shows a level within a range. Progress through a task belongs in a progress element.
:invalid matches before the user types, so every required field looks wrong on page load.
A custom error stays until you clear it with an empty string, so the field never becomes valid again.
Questions people ask
Is HTML form validation enough on its own?
No. It helps users fix mistakes quickly, but anyone can bypass it. Always validate again on the server.
How do I change the browser's validation message?
Use setCustomValidity() in JavaScript, or add novalidate and show your own messages next to each field.
Why does my required field look red before I type?
:invalid matches as soon as the page loads. Style :user-invalid instead so errors appear only after interaction.
What is the difference between progress and meter?
progress shows how much of a task is done. meter shows a measurement within a known range, like battery level.
Why is my pattern ignored?
An invalid regular expression is skipped without warning. Paste it into new RegExp(pattern, "v") in the Console to see the error.
Do I need aria-invalid?
Native validation already exposes invalid state. Add aria-invalid="true" when you run your own checks with novalidate, and remove it once the field is fixed.