We all know that online forms can be a hassle to have to fill out. Especially when there is nothing but a list of inputs presented without much guidance or feedback. But as form creators, there are many things we can do to improve the experience for the users filling out forms online.

Using CSS

Using CSS, we have access to four states of a form input with pseudo-classes - :valid, :invalid, :required and :optional. We can use these states to provide some - albeit limited - feedback to users as they fill out the form.

Using :valid and :invalid, we can let users know, in real time, if their input satisfies all the requirements necessary.

input:valid { border-color: green; }  
input:invalid { border-color: red; }  

Styling valid and invalid pseudo-classes

One problem with using this method, however, is that the styles are applied even before the user starts to act on the form. For required inputs, they will show us as :invalid, and optional inputs will show as :valid. This means that before the user is even given the chance to start, they may already be given negative feedback, which isn’t a great experience.

Styling :required and :optional states alone is typically not very useful either as this is information that is usually given in a label. However, we may want to combine these states with the :valid/:invalid pseudo-classes and only style certain combinations. For example, we may want to only give positive feedback when a required input is valid.

input:required:valid { border-color: green; }  

Styling on valid required inputs

Using JavaScript

Using JavaScript, we can create a much better user experience when filling out forms. For example, consider these three number inputs, each of them requiring a minimum value of 10, a maximum of 100, and a step of 10.

<form>  
    <label>
        Number Input 1
     <input type="number" min="10" max="100" step="10">
    </label>
    <label>
        Number Input 2
     <input type="number" min="10" max="100" step="10">
    </label>
    <label>
        Number Input 3
     <input type="number" min="10" max="100" step="10">
    </label>

    <input type="submit">
</form>  

By setting the min, max and step attributes, we can control the number the user inputs only if they use the controls on the input element. However, this doesn’t stop the user from manually typing in invalid numbers. If they type in 1, 12, and 123 respectively to the three inputs and submits the form, this is what happens -

Default validation message tooltip

What happens is the user only gets an error message for the first input and they only get one error message, even though the input violates 2 of the requirements. We can fix this by modifying the validity message being passed.

Adding multiple error messages to one tooltip

When validating inputs, the browser does checks for a defined list of potential invalidites. There is an object, validity, that is attached to all inputs. It contains a list of keys with boolean values, each checking if a certain type of invalidity applies to this input. For example, this is the validity object returned when the user inputs 1 on the previous input.

input.validity = {  
    valid:false // If the input is valid
    customError:false // If a custom error message has been set
    patternMismatch:false // If the invalidity is against the pattern attribute
    rangeOverflow:false // If the invalidity is against the max attribute
    rangeUnderflow:true // If the invalidity is against the min attribute
    stepMismatch:true // If the invalidity is against the step attribute
    tooLong:false // If the invalidity is against the maxlength attribute
    tooShort:false // If the invalidity is against the minlength attribute
    typeMismatch:false // If the invalidity is against the type attribute
    valueMissing:false // If the input is required but empty
}

By default, the browser will only display one of these invalidities. So what we can do instead is check each of these values ourselves and, if we find an invalidity, store it. Once we have stored all the invalidity messages for an input, we can then set the whole list as a custom validity message using the setCustomValidity() function.

function CustomValidation() { }  
CustomValidation.prototype = {  
    // Set default empty array of invalidity messages
    invalidities: [],

    // Function to check validity
    checkValidity: function(input) {

        var validity = input.validity;

        if ( validity.patternMismatch ) {
            this.addInvalidity('This is the wrong pattern for this field');
        }
        if ( validity.rangeOverflow ) {
            var max = getAttributeValue(input, 'max');
            this.addInvalidity('The maximum value should be ' + max);
        }
        if ( validity.rangeUnderflow ) {
            var min = getAttributeValue(input, 'min');
            this.addInvalidity('The minimum value should be ' + min);
        } 
        if ( validity.stepMismatch ) {
            var step = getAttributeValue(input, 'step');
            this.addInvalidity('This number needs to be a multiple of ' + step);
        }
        // Additional validity checks here...
    },

    // Add invalidity message to invalidities object
    addInvalidity: function(message) {
        this.invalidities.push(message);
    },

    // Retrieve the invalidity messages
    getInvalidities: function() {
        return this.invalidities.join('. \n');
    }
};

// On click of form submit buttons
submit.addEventListener('click', function(e) {  
    // Loop through all inputs
    for ( var i = 0; i < inputs.length; i++ ) {

        var input = inputs[i];

        // Use native JavaScript checkValidity() function to check if input is valid
        if ( input.checkValidity() == false ) {

            var inputCustomValidation = new CustomValidation(); // New instance of CustomValidation
            inputCustomValidation.checkValidity(input); // Check Invalidities
            var customValidityMessage = inputCustomValidation.getInvalidities(); // Get custom invalidity messages
            input.setCustomValidity( customValidityMessage ); // set as custom validity message

        } // end if
    } // end loop
});

Doing, this, when the form from above is submitted again, this is what happens -

Displaying multiple error messages to one tooltip

This is better, because all the error messages for that input are now being displayed. However, we still have the problem that only the error message for the first input is being displayed.

This is a limitation of the native browser validation message. So, we need to find an alternative.

Displaying all error messages for all inputs

Instead of using the built-in tooltip, we can add the error messages to the DOM directly. This way, all the error messages for each input will be clearly displayed next to them.

We can do this very simply with a couple of additional lines -

CustomValidation.prototype.getInvaliditiesForHTML = function() {  
    return this.invalidities.join('. <br>');
}

// On click of form submit buttons
submit.addEventListener('click', function(e) {  
    // Loop through all inputs
    for ( var i = 0; i < inputs.length; i++ ) {

        var input = inputs[i];

        // Use native JavaScript checkValidity() function to check if input is valid
        if ( input.checkValidity() == false ) {

            var inputCustomValidation = new CustomValidation(); // New instance of CustomValidation
            inputCustomValidation.checkValidity(input); // Check Invalidities
            var customValidityMessage = inputCustomValidation.getInvalidities(); // Get custom invalidity messages
            input.setCustomValidity( customValidityMessage ); // set as custom validity message

            // DISPLAY ERROR MESSAGES IN DOCUMENT
            var customValidityMessageForHTML = inputCustomValidation.getInvaliditiesForHTML();
            input.insertAdjacentHTML('afterend', '<p class="error-message">' + customValidityMessageForHTML + '</p>')
            stopSubmit = true;

        } // end if
    } // end loop
    if ( stopSubmit ) { e.preventDefault(); }
});

Now, this is what happens when the user clicks submit -

Displaying all error messages for all inputs in the DOM

Using custom validation checks

Sometimes, the validation checks available in the browser may not be enough. We may want to check the inputted value against some custom requirements, for example if a text input requires special characters.

Since we are already checking for each validity message separately in our CustomValidation.prototype.checkValidity function, we can easily add additional checks here as well.

CustomValidation.prototype.checkValidity = function(input) {

    // In-built validity checks here ------

    // Custom Validity checks ------
    if ( !input.value.match(/[a-z]/g) ) {
        this.addInvalidity('At least 1 lowercase letter is required');
    }
    if ( !input.value.match(/[A-Z]/g) ) {
    this.addInvalidity('At least 1 uppercase letter is required');
    }
}

Realtime validation

Although this is a lot better, it is not without its flaws, the biggest being that the user has to submit the form before they get any specific feedback on what exactly is wrong with their inputs. However, the best user experience for forms is when validation happens in real time. There are three parts to this -

  1. The requirements for each input are clearly displayed even before the user starts typing
  2. As the user starts typing on the field and fulfils each requirement, they are given real time feedback on their success (or failings)
  3. The feedback is presented in such a way that the user will not submit the form without knowing of the errors in their input

In next week's article (see here), I'm going to show how I implement real-time validation, by recreating this simple registration form -

Real-time Validation Demo

If you'd like to try recreating this (or a better version) yourself before then, you can use this boilerplate code.