Building HTML5 Form Validation Bubble Replacements

bubble_header

I’ve written and spoken about HTML5 form validation over the last few years, and one of the most common questions I get is about bubbles. By bubbles I mean the UI controls browsers display validation errors in. Below you can see Chrome’s, Firefox’s, and IE’s implementations:

bubbles

For whatever reason, we developers (or more likely our designer colleagues) have a deep-seated desire to style these things. But unfortunately we can’t, as zero browsers provide styling hooks that target these bubbles. Chrome used to provide a series of vendor prefixed pseudo-elements (::-webkit-validation-bubble-*), but they were removed in Chrome 28.

So what’s a developer to do? Well, although browsers don’t allow you to customize their bubbles, the constraint validation spec does allow you to suppress the browser’s bubble UI and build your own. The rest of the article shows you how to do just that.

Warning: Don’t go down the path of building a bubble replacement lightly. With the default bubbles you get some pretty complex functionality for free, such as positioning and accessibility. With custom bubbles you have to address those concerns yourself (or use a library that does).

Suppressing the Default Bubbles

The first step to building a custom UI is suppressing the native bubbles. You do that by listening to each form control’s invalid event and preventing its default behavior. For example, the following form uses no validation bubbles because of the event.preventDefault() call in the invalid event handler.

<form>
    <input required>
    <button>Submit</button>
</form>
<script>
    document.querySelector( "input" ).addEventListener( "invalid",
        function( event ) {
            event.preventDefault();
        });
</script>

The invalid event does not bubble (no pun intended), so if you want to prevent the validation bubbles on multiple elements you must attach a capture-phase listener.

If you’re confused on the difference between the bubbling and capturing phases of DOM events, check out this MDN article for a pretty good explanation.

The following code prevents bubbles on both inputs with a single invalid event listener on the parent <form> element:

<form>
    <input required>
    <input required>
    <button>Submit</button>
</form>
<script>
    document.querySelector( "form" )
        .addEventListener( "invalid", function( event ) {
            event.preventDefault();
        }, true );
</script>

You can use this approach to remove the browser’s UI for form validation, but once you do so, you have build something custom to replace it.

Building alternative UIs

There are countless ways of displaying form validation errors, and none of them are necessarily right or wrong. (Ok, there are some wrong ways of doing this.) Let’s look at a few common approaches that you may want to take. For each, we’ll use the very simple name and email address form below. We’ll also use some simple CSS to make this form look halfway decent.

<form>
    <div>
        <label for="name">Name:</label>
        <input id="name" name="name" required>
    </div>
    <div>
        <label for="email">Email:</label>
        <input id="email" name="email" type="email" required>
    </div>
    <div>
        <button>Submit</button>
    </div>
</form>

One important note before we get started: all of these UIs only work in Internet Explorer version 10 and up, as the constraint validation API is not available in older versions. If you need to support old IE and still want to use HTML5 form validation, check out this slide deck in which I outline some options for doing so.

Alternative UI #1: List of messages

A common way of showing validation errors is in a box on the top of the screen, and this behavior is relatively easy to build with the HTML5 form validation APIs. Before we get into the code, here’s what it looks like in action:

Here’s the code you need to build that UI:

function replaceValidationUI( form ) {
    // Suppress the default bubbles
    form.addEventListener( "invalid", function( event ) {
        event.preventDefault();
    }, true );

    // Support Safari, iOS Safari, and the Android browser—each of which do not prevent
    // form submissions by default
    form.addEventListener( "submit", function( event ) {
        if ( !this.checkValidity() ) {
            event.preventDefault();
        }
    });

    // Add a container to hold error messages
    form.insertAdjacentHTML( "afterbegin", "<ul class='error-messages'></ul>" );

    var submitButton = form.querySelector( "button:not([type=button]), input[type=submit]" );
    submitButton.addEventListener( "click", function( event ) {
        var invalidFields = form.querySelectorAll( ":invalid" ),
            listHtml = "",
            errorMessages = form.querySelector( ".error-messages" ),
            label;

        for ( var i = 0; i < invalidFields.length; i++ ) {
            label = form.querySelector( "label[for=" + invalidFields[ i ].id + "]" );
            listHtml += "<li>" + 
                label.innerHTML +
                " " +
                invalidFields[ i ].validationMessage +
                "</li>";
        }

        // Update the list with the new error messages
        errorMessages.innerHTML = listHtml;

        // If there are errors, give focus to the first invalid field and show
        // the error messages container
        if ( invalidFields.length > 0 ) {
            invalidFields[ 0 ].focus();
            errorMessages.style.display = "block";
        }
    });
}

// Replace the validation UI for all forms
var forms = document.querySelectorAll( "form" );
for ( var i = 0; i < forms.length; i++ ) {
    replaceValidationUI( forms[ i ] );
}

This example assumes that each form field has a corresponding <label>, where the id attribute of the form field matches the for attribute of the <label>. You may want to tweak the code that builds the messages themselves to match your applications, but other than that this should be something can simply drop in.

Alternative UI #2: Messages under fields

Sometimes, instead of showing a list of messages on the top of the screen you want to associate a message with its corresponding field. Here’s a UI that does that:

To build this UI, most of the code remains the same as the first approach, with a few subtle differences in the submit button’s click event handler.

function replaceValidationUI( form ) {
    // Suppress the default bubbles
    form.addEventListener( "invalid", function( event ) {
        event.preventDefault();
    }, true );

    // Support Safari, iOS Safari, and the Android browser—each of which do not prevent
    // form submissions by default
    form.addEventListener( "submit", function( event ) {
        if ( !this.checkValidity() ) {
            event.preventDefault();
        }
    });

    var submitButton = form.querySelector( "button:not([type=button]), input[type=submit]" );
    submitButton.addEventListener( "click", function( event ) {
        var invalidFields = form.querySelectorAll( ":invalid" ),
            errorMessages = form.querySelectorAll( ".error-message" ),
            parent;

        // Remove any existing messages
        for ( var i = 0; i < errorMessages.length; i++ ) {
            errorMessages[ i ].parentNode.removeChild( errorMessages[ i ] );
        }

        for ( var i = 0; i < invalidFields.length; i++ ) {
            parent = invalidFields[ i ].parentNode;
            parent.insertAdjacentHTML( "beforeend", "<div class='error-message'>" + 
                invalidFields[ i ].validationMessage +
                "</div>" );
        }

        // If there are errors, give focus to the first invalid field
        if ( invalidFields.length > 0 ) {
            invalidFields[ 0 ].focus();
        }
    });
}

// Replace the validation UI for all forms
var forms = document.querySelectorAll( "form" );
for ( var i = 0; i < forms.length; i++ ) {
    replaceValidationUI( forms[ i ] );
}

Alternative UI #3: Replacement bubbles

The last UI I’ll present is a way of mimicking the browser’s validation bubble with a completely custom (and styleable) bubble built with JavaScript. Here’s the implementation in action:

In this example I’m using a Kendo UI tooltip because I don’t want to worry about handling the positioning logic of the bubbles myself. The code I’m using to build this UI is below. For this implementation I chose to use jQuery to clean up the DOM code (as Kendo UI depends on jQuery).

$( "form" ).each(function() {
    var form = this;

    // Suppress the default bubbles
    form.addEventListener( "invalid", function( event ) {
        event.preventDefault();
    }, true );

    // Support Safari, iOS Safari, and the Android browser—each of which do not prevent
    // form submissions by default
    $( form ).on( "submit", function( event ) {
        if ( !this.checkValidity() ) {
            event.preventDefault();
        }
    });

    $( "input, select, textarea", form )
        // Destroy the tooltip on blur if the field contains valid data
        .on( "blur", function() {
            var field = $( this );
            if ( field.data( "kendoTooltip" ) ) {
                if ( this.validity.valid ) {
                    field.kendoTooltip( "destroy" );
                } else {
                    field.kendoTooltip( "hide" );
                }
            }
        })
        // Show the tooltip on focus
        .on( "focus", function() {
            var field = $( this );
            if ( field.data( "kendoTooltip" ) ) {
                field.kendoTooltip( "show" );
            }
        });

    $( "button:not([type=button]), input[type=submit]", form ).on( "click", function( event ) {
        // Destroy any tooltips from previous runs
        $( "input, select, textarea", form ).each( function() {
            var field = $( this );
            if ( field.data( "kendoTooltip" ) ) {
                field.kendoTooltip( "destroy" );
            }
        });

        // Add a tooltip to each invalid field
        var invalidFields = $( ":invalid", form ).each(function() {
            var field = $( this ).kendoTooltip({
                content: function() {
                    return field[ 0 ].validationMessage;
                }
            });
        });

        // If there are errors, give focus to the first invalid field
        invalidFields.first().trigger( "focus" ).eq( 0 ).focus();
    });
});

Although replacing the validation bubbles requires an unfortunate amount of code, this does come fairly close to replicating the browser’s implementation. The difference being that the JavaScript implementation is far more customizable, as you can change it to your heart’s desire. For instance, if you need to add some pink, green, and Comic Sans to your bubbles, you totally can now:

The Kendo UI tooltip widget is one of the 25+ widgets available in Kendo UI Core, the free and open source distribution of Kendo UI. So you can use this code today without worrying about licensing restrictions — or paying money. You can download the Kendo UI core source directly, use our CDN, or grab the library from Bower (bower install kendo-ui-core).

Wrapping Up

Although you cannot style the browser’s validation bubbles, you can suppress them and build whatever UI you’d like. Feel free to try and alter the approaches shown in this article to meet your needs. If you have any other approaches you’ve used in the past feel free to share them in the comments.

Comments

    • Good catch! I actually can’t believe I missed that. I switched the example to use insertAdjacentHTML. Thanks.

  • Pingback: Front End Weekly 3 - VoidHorde : Developer Horde()

  • Nice article TJ! To suppress the default validation UI, I just set `novalidate=”true”` on the form. You can still query the constraintValidationAPI and find out which fields are valid, it seems ‘novalidate’ just suppresses the UI.

  • Pingback: November Bunkerjs | joeldart()

  • Spyros Tsompanakis

    Hi TJ, great article!
    Is there a way to include validation for a select field? I’m using your alternative #2. Here is the code for the select field:
    Breakfast/Grilling

    Please, choose an option from the list!
    Breakfast
    Grilling

    Thanks,
    Spyros

  • Spyros Tsompanakis

    Hi TJ, great article!
    Is there a way to include validation for a select field? I’m using your alternative #2. Here is the code for the select field:
    Breakfast/Grilling

    Please, choose an option from the list!
    Breakfast
    Grilling

    Thanks,
    Spyros

  • Pingback: HTML5 Form Validation with Constraint API | Kevin Chappell()

  • Pingback: Bruce Lawson’s personal site  : Reading List()

  • patrick h. lauke

    “With the default bubbles you get some pretty complex functionality for free, such as positioning and accessibility.”
    Just on the accessibility point, it’s worth noting:
    – on Windows, Chrome’s default validation bubbles are not exposed at all to JAWS nor NVDA; same for IE11’s default bubbles (Firefox and JAWS/NVDA, as well as Edge and NVDA, do expose them correctly though – admittedly, only tested using required attribute, so can’t guarantee all types of native bubbles do)
    – on Android, Chrome’s native bubbles are not exposed to TalkBack
    – on iOS, there are no native bubbles at all anyway

    So, currently custom form validation bubbles may actually be the best solution, sadly.

    • patrick h. lauke

      Worth noting that (from a cursory test) your 3rd example works fairly well with screen readers, but the previous examples are less than ideal…

      Alternative UI #1: the list of error messages is not announced to assistive tech when it appears – worth adding a role=”alert” and/or aria-live=”assertive” to the

      Alternative UI #2: the error message needs to be associated with the relevant input – give the an (auto-generated?) id and tie the input to it with aria-describedby=”[the id of the error message]”

    • patrick h. lauke

      for completeness, just checked on Mac/OS X as well:
      – Safari has no native validation bubbles
      – Chrome and Firefox do have native bubbles, but these are not exposed to/announced by VoiceOver

  • Mahrukh Mehmood

    How can i show error messages in front of the label field like we have Name: (Error Message)

  • Pingback: How to style the HTML5 form validation messages? - html5()