Client‑Side Form Validation with the HTML Constraint Validation API

Hero image for Client‑Side Form Validation with the HTML Constraint Validation API. Image by mymind.
Hero image for 'Client‑Side Form Validation with the HTML Constraint Validation API.' Image by mymind.

In Brief

HTML attributes such as required, type, and pattern define constraints on form controls. JavaScript can inspect them with checkValidity() and validity, or add a rule with setCustomValidity(). Keep instructions and errors in text, associate them with the field, and validate everything again on the server. Browser validation is useful feedback, not a security boundary. Native messages vary, so do not rely on their visual presentation alone.

Form validation often starts as a collection of if statements attached to a submit button. Before long, the same rules are repeated in the HTML, in JavaScript, and on the server, with three slightly different ideas of what "valid" means.

HTML already has a constraint model. Used carefully, it can handle common clientside checks, expose the reason a field is invalid, and leave the form usable when JavaScript is unavailable. It does not remove the need for serverside validation, and it does not guarantee that every browser presents errors in the same way.


Put the Simple Rules in the Markup

Start with the constraints the browser can already describe. The same principle as the fundamentals of HTML applies here: use the element and attributes that express the job before adding script.

<form id="contact-form" action="/contact" method="post">  <label for="email">Email address</label>  <p id="email-hint">We will use this to reply to your enquiry.</p>  <input    id="email"    name="email"    type="email"    required    aria-describedby="email-hint email-error"  >  <p id="email-error" class="error"></p>  <label for="reference">Booking reference</label>  <p id="reference-hint">Enter the six letters and numbers from your email.</p>  <input    id="reference"    name="reference"    type="text"    pattern="[A-Za-z0-9]{6}"    required    aria-describedby="reference-hint reference-error"  >  <p id="reference-error" class="error"></p>  <button type="submit">Send enquiry</button></form>

required rejects an empty value. type="email" asks the browser to check the broad shape of an email address. pattern applies a regular expression to the whole value. These are clientside constraints, not a promise that the data is genuine. An address can have a valid shape and still belong to nobody.

The label tells the user what the control is. The hint explains the required format before an error occurs. That is better than expecting a placeholder to do both jobs, especially when the placeholder disappears as soon as somebody starts typing.


Ask the Control Why It is Invalid

Every candidate form control has a validity property. Controls such as disabled inputs and buttons can be barred from constraint validation; the willValidate property reports whether a particular control participates. For a candidate control, validity returns a ValidityState object with flags such as:

  • valueMissing for an empty required control;
  • typeMismatch when a value does not satisfy a type such as email;
  • patternMismatch when the value fails its pattern;
  • customError when script has set a custom error;
  • valid when none of the constraints fail.

Calling checkValidity() on a control returns true or false. Calling it on the form checks its candidate controls and returns whether they all pass. Invalid controls also receive an invalid event.

The HTML 5.1 draft also defines reportValidity(), which asks the browser to check the constraints and present its validation interface. That presentation was not dependable across the browsers in use in July 2016, so it is a poor baseline for a progressively enhanced form.

The event does not bubble, so a listener on the form needs to use event capture:

var form = document.getElementById('contact-form');form.addEventListener('invalid', function (event) {  if (form.className.indexOf('was-submitted') === -1) {    form.className += ' was-submitted';  }  event.target.setAttribute('aria-invalid', 'true');}, true);form.addEventListener('input', function (event) {  if (event.target.checkValidity()) {    event.target.removeAttribute('aria-invalid');  }});

This is deliberately small. It preserves the browser's constraint model instead of copying every validation rule into a second JavaScript object.


Add a Custom Constraint Without Taking Over

Some checks depend on more than one field. Confirming an email address is a typical example. setCustomValidity() lets that rule join the browser's existing model:

<label for="email-confirmation">Confirm email address</label><input  id="email-confirmation"  name="email-confirmation"  type="email"  required>
var email = document.getElementById('email');var confirmation = document.getElementById('email-confirmation');function validateConfirmation() {  if (confirmation.value !== email.value) {    confirmation.setCustomValidity('The email addresses do not match.');  } else {    confirmation.setCustomValidity('');  }}email.addEventListener('input', validateConfirmation);confirmation.addEventListener('input', validateConfirmation);

The empty string is important. A custom error remains active until it is cleared, even after the values have become acceptable. That is an easy way to create a form which insists it is wrong whilst displaying two identical addresses.

Keep custom rules deterministic and local. Checks that require a database, permission, or current account state belong on the server. An asynchronous username lookup can improve feedback, but it cannot authorise the eventual submission.


Do Not Leave the Error Inside a Bubble

Browsers choose their own wording and presentation for native validation messages. In July 2016, support was not uniform: WebKit allowed scripts to inspect validity, but Safari did not yet perform interactive validation on submission. A form therefore needed to remain understandable without depending on one browser's validation bubble.

When custom presentation is required, use the validity flags to select a short text message and place it beside the relevant field. Keep the message's identifier in aria-describedby, set aria-invalid="true" only after a failed attempt, and move focus to the first invalid field when submission is stopped. The WCAG 2.0 guidance on input errors requires an identified error to be described in text.

Colour can support that message, but it cannot replace it. This CSS is useful only as an additional cue:

.was-submitted input:invalid {  outline: 2px solid #b00020;}.error {  font-weight: bold;}

Applying :invalid without the .was-submitted guard can make an untouched form look broken as soon as it loads. Add that class when the first invalid event is captured, then update the nearby message from the control's validity state.

If JavaScript is unavailable, the form should still submit to the server. If a browser does not provide interactive validation, the server response should return the entered values, identify each error in text, and associate those errors with the controls again. Progressive enhancement is less impressive than a custom validation framework, but it is much harder to strand.


Submission and Validation are Different Jobs

Clientside validation answers whether the current values satisfy the constraints available to the browser. It does not decide what those values mean to the application and it does not serialise or send them.

When a user activates a submit button, interactive constraint validation happens before the form is submitted. If a control is invalid, its invalid event fires and the normal submit event does not continue. Calling the form's submit() method from script bypasses that interactive validation and does not fire a submit event, so it should not be used as a shortcut around the browser's process.

The related explanation of why HTML form values are strings covers the DOM values JavaScript receives. Submitting forms with FormData covers the later submission step. Keeping those concerns separate prevents the validation code from quietly becoming a second, incomplete server.

Always repeat the checks on receipt. A request can be written by another program, browser validation can be disabled, and clientside code can be altered. The server remains responsible for required data, permitted formats, business rules, authorisation, and safe storage.


Final Thoughts

The constraint validation API is most useful when it reduces code. Put simple rules in the HTML, inspect the browser's validity state when the interface needs more help, and use setCustomValidity() for the occasional rule the markup cannot express.

Then make the failure understandable without colour, a disappearing tooltip, or JavaScript. The browser can improve the conversation with the user. The server still decides whether the answer is acceptable.


Postscript

Aug 2026: This article forms part of an archive restored from a previous version of my website. Its original publication date is accurate. During the restoration, I reviewed and updated it where appropriate for formatting, imagery, broken links, code correctness, and current internal references, whilst preserving the original technical context and intent.

The constraint validation API remains available, but browser presentation and assistivetechnology behaviour continue to vary. Current implementations also include later CSS and platform features that are intentionally outside this 2016 article. Serverside validation remains the authority.


Want to find out more?

If you need senior handson support with a complex React or Next.js platform, migration, performance issue, or technical SEO problem, send me the context and I'll tell you where I can help.