by Wes Goulet published on Dec 28, 2025
I think of client-side validation as a progressive enhancement for your users. You have to validate user input on the server (you can’t trust what comes from the client), but some validation on the client makes for a nice UX. But that doesn’t have to mean lots of JS code or using some validation library on your client. You can get pretty far with the browser’s built-in HTML input validation. And then you can layer a little bit of JS on top of that to make it even better.
Let’s take a look at a text input. You can use pattern, minlength and maxlength to provide constraints. You can use title to provide error message. You can conditionally style invalid input with :user-invalid...
by Wes Goulet published on Dec 28, 2025
I think of client-side validation as a progressive enhancement for your users. You have to validate user input on the server (you can’t trust what comes from the client), but some validation on the client makes for a nice UX. But that doesn’t have to mean lots of JS code or using some validation library on your client. You can get pretty far with the browser’s built-in HTML input validation. And then you can layer a little bit of JS on top of that to make it even better.
Let’s take a look at a text input. You can use pattern, minlength and maxlength to provide constraints. You can use title to provide error message. You can conditionally style invalid input with :user-invalid.
Example
<label for="program_name">Name of program</label><input type="text" id="program_name" minlength="3" maxlength="20" pattern="[a-zA-Z0-9]+" title="Only alphabetical and numerical characters are accepted" required/>
input:user-invalid { border: 4px solid red;}
When the user attempts to submit the form the browser takes care of validating and showing the message.
Example validation message on Chrome
Example validation message on Firefox
Example validation message on Safari
Example validation message on Safari on iOS
You can play with a live example at this CodePen.
BTW, I just noticed that Chrome and Firefox say "Please" but Safari doesn’t 😃
Styling
You can’t style the error popup, so if that’s important to you then maybe you need to write your own error UI. A lot of times I don’t mind leaning on browser UI when it’s available (ie: most of the time I don’t need my error messages in my website’s overall brand/styling). Also, I think a lot of users have seen the browser’s error UI before (from other sites that use native form validation), so there is some familiarity there for the user.
The Problem: Accessibility
Unfortunately, native form validation isn’t very accessible.
I had assumed it was accessible, because most of the time when I lean on the browser to do something it handles accessibility much better than any userland code I would write. But after reading this excellent post from Adrian Roselli (an accessibility expert), I learned my assumption was wrong.
The main accessibility issues with native form validation are:
- Error messages aren’t associated with form fields - Screen readers don’t reliably announce which field has an error, so users relying on assistive technology can’t easily figure out what needs to be fixed.
- Error messages disappear too quickly - The browser’s error bubble might disappear before users can read it.
- Error messages don’t respect user text size/spacing preferences - The error bubble text doesn’t resize with browser zoom settings or respect text spacing preferences.
Related to all this, a recent update to WCAG acknowledges these accessibility issues with native form validation.
Make it better with JS
I originally wrote this post to point out that native form validation is good enough (lean on the browser!) and that’s all you need. But after reading about the accessibility issues, I think the right answer is using native form validation as the foundation, and then adding a bit of JS to make it more accessible.
We’ll use the Constraint Validation API, which is the JavaScript interface for HTML form validation. It allows you to programmatically check if a form field is valid, get validation messages, and customize how errors are displayed to users. For example, you can check if an input is valid:
const element = document.getElementById("program_name");console.log(element.checkValidity()); // returns true or false
You can also get the validation message:
const element = document.getElementById("program_name");console.log(element.validationMessage); // returns the error message if invalid
We’ll use the Constraint Validation API and follow WCAG input error guidance to create our own error messages that are properly associated with form fields. Here’s a simple example:
<form id="my-form"> <label for="program_name">Name of program</label> <input type="text" id="program_name" minlength="3" maxlength="20" pattern="[a-zA-Z0-9]+" title="Only alphabetical and numerical characters are accepted" required aria-describedby="program_name-error" /> <span id="program_name-error" role="alert"></span></form>
const form = document.getElementById("my-form");const input = document.getElementById("program_name");const errorMessage = document.getElementById("program_name-error");// IMPORTANT: set this attribute in JS, that way it's a// progressive enhancement (ie: if JS isn't available the// native form validation will still work).form.setAttribute("novalidate", "");function validateInput() { const isValid = input.checkValidity(); errorMessage.textContent = isValid ? "" : input.validationMessage; if (isValid) { input.removeAttribute("aria-invalid"); } else { input.setAttribute("aria-invalid", "true"); }}// Validate on blur (when user leaves the field)input.addEventListener("blur", validateInput);// Clear errors as user typesinput.addEventListener("input", () => { if (input.checkValidity()) { errorMessage.textContent = ""; input.removeAttribute("aria-invalid"); }});// Handle form submitform.addEventListener("submit", (e) => { if (!form.checkValidity()) { e.preventDefault(); // Update validation state for all fields validateInput(); }});
In this example:
- The form has
novalidateto turn off the browser’s built-in validation (added via JavaScript so it degrades gracefully). - The error message is associated with the input using
aria-describedby, so screen readers will announce it when the field is focused. - The error message has
role="alert"so screen readers will announce it when it appears. - The input gets
aria-invalidset appropriately, clearly marking it for assistive technologies. - Validation happens on
blur(when the user leaves the field) and on form submit. - The error message stays visible, giving users time to read it.
You can play with a live example at this CodePen.
This post by Cloud Four spells out a more complete solution in detail. (If you mainly support evergreen browsers then I wouldn’t worry too much about the first part "Removing invalid styles on page load for all browsers" since Chrome has shipped support for :user-invalid for a couple years now.)
So native HTML form validation is a good starting point, but it’s not enough on its own due to accessibility issues. You can use the Constraint Validation API to layer on accessible error messages with a bit of JavaScript. That way you get the browser’s validation working as a baseline, and then enhance it to be accessible when JS is available.
Thanks to Manuel for reviewing this post and pointing out the accessibility issues with native form validation.
About Wes Goulet
Maker of (hopefully) useful things on the web. Microsoft/Salesforce alum. Big fan of PWAs, SSGs, web standards, and simple/boring code.
Site: goulet.dev