One of the most common uses of JavaScript in web applications is form validation. Client-side form validation gives users near-immediate feedback about whether or not their input data is valid. Developers tend to reach for one of many excellent form validation libraries, particularly if they are working within the context of a framework like React or Angular, where many such solutions exist.
These libraries add value to your application, but even when you may only need a small portion of a library’s functionality, they come at a cost, particularly to your bundle size.
If it aligns with your use case, there may be a simpler way. In this article, we’ll dive deep into all things HTML form validation, including:
ValidityState
checkValidity
HTML5 and the modern DOM APIs are already equipped for form validation. HTML5 arrived in 2008 and brought with it many improvements, including the input
tag, which received several updates around form validation.
HTML5 added new attributes that declaratively set validation rules for a given input field. These new attributes include:
required
: Specifies that the field can’t be blankmin
/max
: Specifies the range of allowed values for numeric inputsminlength
/maxlength
: Specifies the range of allowed length for text inputsstep
: For a step of n
, a numeric input value is only valid if it is a multiple of n
pattern
: Specifies a regular expression that a text input must matchHTML5 also introduced some new CSS selectors, including two new pseudo-classes — :valid
and :invalid
. These match any input whose value has passed or failed validation, respectively.
For example, we can automatically mark invalid fields with a red border:
input:invalid { border: 2px solid red; } input:valid { border: 2px solid green; }
Before entering a value in this required field, it is automatically marked as invalid:
When a value is entered, the :invalid
selector no longer matches and, instead, the :valid
one does:
If you attempt to submit a form that contains an input with an invalid value, the form submission will be canceled and a browser-supplied error message will be shown (the below shows it as it appears in Chrome):
This may not be ideal for your application. The look and feel of the message varies across browsers, and may not fit in consistently with the rest of your UI.
For this reason, these validation messages are optional. You can opt out of them by setting the novalidate
attribute on the form containing the input:
<form name="loginForm" novalidate> Username: <input type="text" name="username" required> </form>
This disables only the validation behavior — the rules are still in place. If the form contains an invalid input, it will still be submitted. If you set the novalidate
attribute, the form will no longer automatically validate its fields on submit. You will have to do this manually in such a case by calling checkValidity
on the form and stopping form submission if the form is invalid.
The advantage is that you have full control over the validation process and its behavior. The validation rules and current validation state are still provided to you via the Constraint Validation API.
This DOM API goes hand in hand with the HTML5 validation attributes we discussed above. It provides some objects, methods, and events that allow you to build the validation behavior without worrying about checking the validation rules themselves.
ValidityState
The input tags within a form have a validity
property, which is a ValidityState
object and reflects the current validation state of the input. The value is updated in real time; it always reflects the validation state at that moment in time.
ValidityState
has a series of boolean flags. The primary one is simply valid
— this is true
if the element passes all applicable status checks. As soon as a valid input becomes invalid due to a change in its value, this flag immediately becomes false
.
If an element’s validity.valid
property is false
, there are additional flags that can be checked in ValidityState
to determine which validation rule is being violated. Different groups of these flags apply to different validation rule types.
ValidityState
flagsValidation rule | Associated validity flags |
required |
|
min, max |
|
minlength, maxlength |
|
step |
|
pattern |
|
type="email", type="url" |
|
checkValidity
A form is checked for validity when a user attempts to submit it, but you may want your form to flag invalid fields sooner (perhaps on blur, or even on change).
This can be done by calling the checkValidity
method on a particular input, or on the enclosing form element.
When checkValidity
is called on a form, it returns true
if all fields are valid. If any fields are invalid, the form will return false
and each invalid field will individually fire an invalid
event. It behaves similarly when called on a single input, except that it checks and potentially fires the event for that input only.
See the Pen
Custom validation code by Joe Attardi (@thinksInCode)
on CodePen.
In the above CodePen, we manually trigger validation any time the field changes. If the form is submitted with the empty field, it shows the validation error, but as soon as you type a single character, the error message goes away. If you delete the contents of the field, the validation error will immediately reappear.
You may encounter a situation where none of the inbuilt validation checks are applicable to your validation rules. For example, you may have password and password confirmation fields in your signup form. If these passwords don’t match, the fields should be marked as invalid.
To support this use case, all inputs have a setCustomValidity
method. This will flag the field as invalid and set the specified message as a validation error.
Note that this only sets the validation state to invalid. It doesn’t perform any kind of checks on the value; you will need to implement the custom logic. Here’s a simple example:
function validatePasswords() { if (passwordField.value !== confirmPasswordField.value) { confirmPasswordField.setCustomValidity('Password does not match'); } }
When setCustomValidity
is called on the confirmPasswordField
input above:
validity.valid
flag becomes false
validity.customError
flag becomes true
validationMessage
property becomes “Password does not match”Let’s consider a more complex example for a signup form. It would be nice to give a new user feedback about whether or not their chosen username is already taken. This can be done asynchronously and the result set with setCustomValidity
.
async function checkUsername() { // We only should make the async check if the username field has a // value, otherwise we will skip this and the blank username field // will be handled when the built-in validation runs (assuming the // username field has the `required` attribute set. if (usernameField.value) { // Use the Fetch API to call our endpoint to validate the username. // The response will contain a `valid` property. const response = await fetch(`/api/checkUsername/${usernameField.value}`); const { valid } = await response.json(); if (!valid) { usernameField.setCustomValidity('This username is already taken.'); } } }
checkValidity
In the above example, if the passwords do not match, calling checkValidity
on the form will still return true
, and no invalid events are fired. You will need to manually check the fields and set the custom validity.
Once an input has a custom error set, however, any further calls to checkValidity
will see the custom error, the validation will fail, and the affected input will fire the invalid
event.
This can be wrapped up in a submit handler for the form:
form.addEventListener('submit', event => { // first check any custom validation logic if (passwordInput.value !== confirmPasswordInput.value) { confirmPasswordInput.setCustomValidity('Passwords do not match'); } // now run the built-in validation, which will detect // any custom validation errors from above // if validation fails, stop the submission if (!form.checkValidity()) { event.preventDefault(); } });
The Constraint Validation API is synchronous. When calling checkValidity
on a form or input, it sets the validation result immediately. If your form contains an input that requires asynchronous validation logic, such as the username check example above, you will need to defer the checkValidity
call until your asynchronous operation is complete and the setCustomValidity
call has been made.
Extending the above submit handler to include the username check might look like this:
form.addEventListener('submit', async event => { // first check any custom validation logic // as shown above, if the username field is blank the async validation // will be skipped, and the required validation rule will be handled // below when we call `form.checkValidity`. await checkUsername(); if (passwordInput.value !== confirmPasswordInput.value) { confirmPasswordInput.setCustomValidity('Passwords do not match'); } // now run the built-in validation, which will detect // any custom validation errors from above // if validation fails, stop the submission if (!form.checkValidity()) { event.preventDefault(); } });
While the API simplifies a lot of the core validation logic, in most cases, you’ll need to do some extra work to fill in the gaps for a fully featured validation solution.
The main limitation with this API is the need to be explicit about some validation operations. If the novalidate
attribute is set — which it usually will be, to disable in-browser messages — the validation flow must be orchestrated manually.
Fortunately, there isn’t much extra code to write. If you want to validate only on submit, the above snippet will do. If you want to validate sooner, such as when an input loses focus, you will need to listen for the blur event and in the event handler, perform any custom validation, then call checkValidity on the input.
If in-browser validation messages are disabled via the novalidate
attribute, a field will be marked invalid in its ValidityState
, but no message will be displayed. The browser still provides a suitable message with the validationMessage
property of the input
element, but you will need to add code to display the message (or provide your own).
An input
can listen for the invalid
event and show an appropriate error message in response to it. If there’s a custom validity message, that can be used, otherwise the message will have to depend on the values in the input’s ValidityState
.
There’s good and bad aspects of this. A core API can’t anticipate all the possible validation scenarios, so there will often be a gap if you have unique validation requirements. However, once you have performed your custom validation checks, you can lean back on the API to get those results into the rest of the form validity state.
For example, a custom validation check might be helpful on a Change Password page, where two fields allow password inputs. A custom validation check can indicate whether the new password and confirm password inputs match. See the below CodePen:
Custom validation check
Validation example of using .setCustomValidity to perform custom validation logic not supported by the core API…
The Constraint Validation API is just one of many handy browser APIs that have been added in recent years. The Constraint Validation API, combined with the HTML5 validation attributes, can save you a lot of boilerplate validation code. For simple applications, it may be all you need.
It has great browser support, too, going back as far as Internet Explorer 10! Older browsers may not support some of the newer features of this API, but the core support is there.
For more advanced validation, or larger scale applications, it may not quite be enough.
Fortunately, the API exposes solid low-level building blocks that can be used to build a more robust validation solution with less code. You’ll need to worry about custom validation, error messaging, and when to validate, but the API takes care of checking and validating fields.
If you’re interested in seeing some further examples of each of these, check out the CodePen Collection I put together.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.