The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
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 eligibilityif (control.willValidate) inspect(control.validity)View examples
Inspect the validity stateconst { valueMissing, typeMismatch, valid } = email.validityView examples
Read the browser messageconst message = control.validationMessageView examples
Check one controlconst valid = control.checkValidity()View examples
Check a whole formconst valid = form.checkValidity()View examples
Report invalid controlsconst valid = form.reportValidity()View examples
Capture invalid eventsform.addEventListener('invalid', handleInvalid, true)View examples
Set a custom errorconfirmation.setCustomValidity('Email addresses must match.')View examples
Clear a custom errorconfirmation.setCustomValidity('')View examples
Request normal submissionform.requestSubmit(saveButton)View examples
Submit without validationHTMLFormElement.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 inputinput: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

01

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.

Describe an account identifier with native constraints
<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.

Constrain a numeric quantity without parsing text manually
<label for="quantity">Quantity</label>
<input id="quantity" name="quantity" type="number" min="1" max="20" step="1" value="1">
Back to quick reference ↑
02

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.

Audit candidate controls in a form
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
  });
}
Back to quick reference ↑
03

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.

Map validity flags to stable inline guidance
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);
Back to quick reference ↑
04

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.

Build an error summary with static validation
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.

Report one field after it has been edited
const postalCode = document.querySelector('#postal-code');
postalCode.addEventListener('blur', () => postalCode.reportValidity());
postalCode.addEventListener('input', () => {
  if (postalCode.validity.valid) postalCode.removeAttribute('aria-invalid');
});
Back to quick reference ↑
05

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.

Validate matching email fields without a stale error
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());
Back to quick reference ↑
06

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.

Submit through a real submitter from custom UI
<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>
Back to quick reference ↑
07

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.

Expose stable inline feedback after attempted submission
<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>
Back to quick reference ↑

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.

Runs in your browser
Preview

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. WHATWGFormshtml.spec.whatwg.org
  2. WHATWGA form control's valuehtml.spec.whatwg.org
  3. WHATWGThe input elementhtml.spec.whatwg.org
  4. World Wide Web Consortium Web Accessibility InitiativeValidating Inputw3.org
  5. MDN Web DocsConstraint validationdeveloper.mozilla.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback