The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Require a value | <input id="email" name="email" type="email" required> | View examples |
| Constrain text length | <input name="username" minlength="3" maxlength="24"> | View examples |
| Require a text pattern | <input
name="code"
pattern="[A-Z]{3}-[0-9]{4}"
title="AAA-1234"> | View examples |
| Constrain a number | <input
name="quantity"
type="number"
min="1"
max="20"
step="1"> | View examples |
| Check validation eligibility | if (control.willValidate) inspect(control.validity) | View examples |
| Inspect the validity state | const { valueMissing, typeMismatch, valid } = email.validity | View examples |
| Read the browser message | const message = control.validationMessage | View examples |
| Check one control | const valid = control.checkValidity() | View examples |
| Check a whole form | const valid = form.checkValidity() | View examples |
| Report invalid controls | const valid = form.reportValidity() | View examples |
| Capture invalid events | form.addEventListener('invalid', handleInvalid, true) | View examples |
| Set a custom error | confirmation.setCustomValidity('Email addresses must match.') | View examples |
| Clear a custom error | confirmation.setCustomValidity('') | View examples |
| Request normal submission | form.requestSubmit(saveButton) | View examples |
| Submit without validation | HTMLFormElement.prototype.submit.call(form) | View examples |
| Disable submit-time validation | <form action="/import" method="post" novalidate> | View examples |
| Bypass validation for one action | <button
type="submit"
formnovalidate
name="action"
value="draft">
Save draft
</button> | View examples |
| Style invalid user input | input:user-invalid { border-color: var(--error-color); } | View examples |
HTML constraint validation is a browser-provided first layer for eligible form controls. Express rules with the correct input type and attributes, keep labels and instructions visible, and use the API only for relationships that native constraints cannot describe. Validation should help users correct data without blocking typing, and the server must independently enforce every security and business rule. Know which submission path invokes validation, clear custom errors as soon as they are resolved, and never replace a native control with an inaccessible imitation just to customize its error UI.
Step by step
Detailed examples
Start with semantic types and declarative constraints
Native attributes define constraints that browsers, assistive technology, password managers, and mobile keyboards can understand. required detects an omitted value; email and url types detect type mismatch; minlength and maxlength constrain user-entered text; pattern applies a full-value JavaScript regular expression; and min, max, and step constrain supported date, time, and number controls. Constraints vary by input type, so confirm that each attribute applies to the chosen control. Keep concise format instructions visible rather than relying on placeholder or title text, and use autocomplete tokens where appropriate.
<label for="account-code">Account code</label>
<p id="account-code-hint">Use three capital letters, a hyphen, and four digits.</p>
<input
id="account-code"
name="accountCode"
pattern="[A-Z]{3}-[0-9]{4}"
aria-describedby="account-code-hint"
autocomplete="off"
required
> Note: The pattern is matched against the entire value. An empty value only fails when required is also present.
<label for="quantity">Quantity</label>
<input id="quantity" name="quantity" type="number" min="1" max="20" step="1" value="1"> Know which controls are candidates for validation
willValidate reports whether a control is currently eligible for constraint validation. Disabled controls, descendants of a disabled fieldset except those in its first legend, button and reset controls, hidden inputs, and other barred elements do not participate; readonly controls that support readonly are also barred. A control without a form owner can still be checked directly when eligible. Being barred does not make bad data safe—it only excludes that control from the browser's constraint-validation process. Do not disable a required field merely to hide an error, and never depend on client validation for authorization or integrity.
const form = document.querySelector('#profile');
for (const control of form.elements) {
if (!('willValidate' in control)) continue;
console.log(control.name, {
candidate: control.willValidate,
valid: control.willValidate ? control.validity.valid : null
});
} Use ValidityState to explain the actual failing rule
A candidate control exposes a live ValidityState. Its flags include valueMissing, typeMismatch, patternMismatch, tooLong, tooShort, rangeUnderflow, rangeOverflow, stepMismatch, badInput, and customError; valid is true only when every failure flag is false. validationMessage contains a browser-localized message, or the custom message when customError applies. Do not infer a specific failure from valid alone. Length constraints have special native semantics: minlength and maxlength are evaluated for user-provided changes, not merely because script assigned a value, even when checkValidity or reportValidity is called afterward.
function explainEmail(control) {
const state = control.validity;
if (state.valueMissing) return 'Enter an email address.';
if (state.typeMismatch) return 'Use an address such as name@example.com.';
if (state.tooLong) return `Use at most ${control.maxLength} characters.`;
if (state.customError) return control.validationMessage;
return '';
}
const email = document.querySelector('#email');
const message = document.querySelector('#email-error');
message.textContent = explainEmail(email); Separate static checks from interactive reporting
checkValidity checks constraints, returns a boolean, and fires a cancelable invalid event at each failing control without asking the browser to display its error UI. reportValidity performs the same checks and, for an invalid event that is not canceled, asks the user agent to report the problem and may focus or scroll to the control. invalid does not bubble, so observe it directly or use capture on the form. Avoid checking on every keystroke: users need room to enter temporarily incomplete values. Validate on submission and optionally after blur or after a field has already been reported invalid.
const form = document.querySelector('#checkout');
const review = document.querySelector('#review');
const summary = document.querySelector('#error-summary');
review.addEventListener('click', () => {
const valid = form.checkValidity();
if (valid) {
summary.hidden = true;
return;
}
const count = form.querySelectorAll(':invalid').length;
summary.textContent = `Correct ${count} highlighted field${count === 1 ? '' : 's'}.`;
summary.hidden = false;
summary.focus();
});
form.addEventListener('invalid', event => {
event.target.setAttribute('aria-invalid', 'true');
}, true); Note: Make the review control type="button" and the summary focusable with tabindex="-1". A normal invalid submission is intercepted before submit fires.
const postalCode = document.querySelector('#postal-code');
postalCode.addEventListener('blur', () => postalCode.reportValidity());
postalCode.addEventListener('input', () => {
if (postalCode.validity.valid) postalCode.removeAttribute('aria-invalid');
}); Set and clear relationship-based custom errors
setCustomValidity adds a customError to one control. Any non-empty message keeps that control invalid until setCustomValidity is called with the empty string, even after the underlying values become correct. Recompute the rule whenever any participating value changes, and attach the error to the control the user should edit. Keep business validation deterministic and synchronous when possible; remote uniqueness checks can become stale and still require server enforcement. A custom message participates in native reporting but should not contain sensitive server details.
const email = document.querySelector('#email');
const confirmation = document.querySelector('#email-confirmation');
function validateConfirmation() {
const mismatch = confirmation.value !== '' && confirmation.value !== email.value;
confirmation.setCustomValidity(mismatch ? 'Email addresses must match.' : '');
}
email.addEventListener('input', validateConfirmation);
confirmation.addEventListener('input', validateConfirmation);
confirmation.addEventListener('blur', () => confirmation.reportValidity()); Choose submission APIs without accidentally bypassing validation
User activation of a submit button performs interactive constraint validation unless the form has novalidate or that submitter has formnovalidate. requestSubmit optionally takes a submit button and follows the same path, including the submitter's override attributes and the submit event. The legacy submit method bypasses both validation and the submit event, so reserve it for intentionally continuing after equivalent checks. A form control named submit can shadow form.submit; calling the prototype demonstrates the true bypass but is rarely desirable. A Save draft button can use formnovalidate, while the server must still distinguish and safely validate that action.
<form id="article" action="/articles" method="post">
<label for="title">Title</label>
<input id="title" name="title" required>
<button id="publish" name="action" value="publish">Publish</button>
<button name="action" value="draft" formnovalidate>Save draft</button>
</form>
<button id="publish-toolbar" type="button">Publish from toolbar</button>
<script>
const form = document.querySelector('#article');
const publish = document.querySelector('#publish');
document.querySelector('#publish-toolbar').addEventListener('click', () => {
form.requestSubmit(publish);
});
</script> Make errors perceivable, reversible, and server-enforced
Native browser messages vary in wording and presentation, so pair every control with a visible label and any format hint needed before submission. :invalid matches as soon as a control is invalid and may paint untouched required fields; :user-invalid is better suited to post-interaction styling where supported. When building custom inline feedback, connect it with aria-describedby, set aria-invalid only after an error is presented, remove it when resolved, and provide an error summary for long forms. Do not rely on color alone, move focus without warning, or erase entered values. Client validation improves interaction only: repeat normalization, type checks, bounds, authorization, and cross-field rules on the server.
<form id="signup">
<label for="signup-email">Email</label>
<input id="signup-email" name="email" type="email" aria-describedby="signup-email-hint signup-email-error" required>
<p id="signup-email-hint">We will send a confirmation link.</p>
<p id="signup-email-error"></p>
<button>Join</button>
</form>
<script>
const form = document.querySelector('#signup');
const email = document.querySelector('#signup-email');
const error = document.querySelector('#signup-email-error');
form.addEventListener('invalid', event => {
event.preventDefault();
event.target.setAttribute('aria-invalid', 'true');
if (event.target === email) error.textContent = email.validationMessage;
}, true);
email.addEventListener('input', () => {
if (!email.validity.valid) return;
email.removeAttribute('aria-invalid');
error.textContent = '';
});
</script> Local code tester
Try native and custom form constraints
Edit the fields to compare built-in email validation with a cross-field custom error. Submission stays inside the sandbox.
Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



