Joe Attardi Software engineer and author focused on frontend/UI topics.

When and how to choose HTML for form validation

7 min read 2133

When And How To Choose HTML For Form Validation

Introduction

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:

HTML5 form validation features

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.

Validation attributes

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 blank
  • min/max: Specifies the range of allowed values for numeric inputs
  • minlength/maxlength: Specifies the range of allowed length for text inputs
  • step: 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 match

CSS selectors

HTML5 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:

Invalid Entry For Required Field

When a value is entered, the :invalid selector no longer matches and, instead, the :valid one does:

Valid Entry

Inbuilt validation messages

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):

Inbuilt Validation

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.

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.

Inspecting validity with 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 flags

Validation rule Associated validity flags
required
  • valueMissing: The input has a blank value
min, max
  • rangeOverflow: The value is less than the minimum allowed value
  • rangeOverflow: The value exceeds the maximum allowed value
minlength, maxlength
  • tooShort: The input value’s length is less than the minimum
  • tooShort: The input value’s length exceeds the maximum
step
  • stepMismatch: The input value falls outside of the allowed step interval
pattern
  • patternMismatch: The input value does not match the given pattern
type="email", type="url"
  • typeMismatch: The input value does not match the specified type. For example, an input with a type of "email" has a value that is not a valid email address

Validating on demand with 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.

Custom validation logic

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:

  • The input’s validity.valid flag becomes false
  • The input’s validity.customError flag becomes true
  • The input’s validationMessage property becomes “Password does not match”

Asynchronous custom validation

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.


More great articles from LogRocket:


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.');
    }
  }
}

Custom validation messages and 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();
  }
});

Integrating asynchronous validation

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();
  }
});

Limitations of the Constraint Validation API

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.

Building your own flow

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.

Displaying error messages

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.

Custom or advanced validation logic

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

const form = document.querySelector(‘form’); const error = document.querySelector(‘.error’); const input = document.querySelector(‘input’); const success = document.querySelector(‘#success’); const inputs = { password: document.querySelector(‘#password’), confirmPassword: document.querySelector(‘#confirmPassword’) } const errorMessages = { password: document.querySelector(‘#password-error’), confirmPassword: document.querySelector(‘#confirmPassword-error’) }; form.addEventListener(‘submit’, event => { success.classList.add(‘hidden’); // Stop the form submission so we don’t leave CodePen!

Conclusion

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.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Joe Attardi Software engineer and author focused on frontend/UI topics.

Leave a Reply