I would like to have a form that:
- Display validation messages in a custom format instead of the default style.
- Display all invalid field bubbles at once instead of one at a time.
Right now, I am stuck with the boring browser-specific message appearance and I don’t see the next error until I correct the last one. This is a really bad user experience, so looking for a few pointers on how to address this.
This is my current JavaScript code:
const contactUsForm = document.querySelector('#Form'); if (contactUsForm) { function Validate() { validatedFields = contactUsForm.querySelectorAll('[data-validation-required],[data-validation-format]'); validatedFields.forEach(field => { /* RegEx patterns */ const emailPattern = /^((([a-z]|d|[!#$%&'*+-/=?^_`{|}~]|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])+(.([a-z]|d|[!#$%&'*+-/=?^_`{|}~]|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])+)*)|((x22)((((x20|x09)*(x0dx0a))?(x20|x09)+)?(([x01-x08x0bx0cx0e-x1fx7f]|x21|[x23-x5b]|[x5d-x7e]|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])|(\([x01-x09x0bx0cx0d-x7f]|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF]))))*(((x20|x09)*(x0dx0a))?(x20|x09)+)?(x22)))@((([a-z]|d|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])|(([a-z]|d|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])([a-z]|d|-|.|_|~|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])*([a-z]|d|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF]))).)+(([a-z]|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])|(([a-z]|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])([a-z]|d|-|.|_|~|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])*([a-z]|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])))$/i; if (field.getAttribute('type') === 'email') { field.setAttribute('pattern', emailPattern); } if (field.validity.valueMissing) { field.setCustomValidity(field.dataset.validationRequired); } else if (field.validity.patternMismatch) { field.setCustomValidity(field.dataset.validationFormat); } else { field.setCustomValidity(''); } field.reportValidity(); contactUsForm.checkValidity(); /* Recheck on field value change */ field.addEventListener('change', function() { field.setCustomValidity(''); Validate(); }); }); } Validate(); contactUsForm.addEventListener('submit', function(e) { e.preventDefault; if (e.checkValidity() == false) { return false; } else { // form.submit() } }); }
Advertisement
Answer
Styling validation bubbles/tooltips used to be feature but only exclusive to Chrome, however it got removed. More information about it here: How do you style the HTML5 form validation messages?
However, you can create your own tooltips or bubbles to display validation messages. With use of a div container and a span and a little bit of CSS, you can create a bubble with almost any kind of look you can image.
.ttCont { position: relative; display: inline-block; } .ttCont .ttText { display: inline-block; visibility: hidden; min-width: 200px; background-color: darkblue; color: #fff; text-align: center; border-radius: 6px; padding: 5px; opacity: 0; transition: opacity 0.5s; /* Place bubble to the right of container */ position: absolute; z-index: 1; top: 5px; left: 105%; } .ttCont .ttText::after { content: " "; position: absolute; top: 50%; right: 100%; /* To the left of the bubble */ margin-top: -5px; border-width: 5px; border-style: solid; border-color: transparent darkblue transparent transparent; } .ttCont .ttText.active{ visibility: visible; opacity: 1; }
Use of custom bubbles now means that you might need to use less of the ValidityState API, however you can still validate your fields using the same approach. Instead of using field.reportValidity()
, you can create a custom function that shows a bubble whenever each field validates
function customReportValidatity(elem, type) { let msg = ""; ///check if validity is based on required or mismatch/// switch (type) { case 'required': msg = $(elem).attr('data-validation-required'); ///without jQuery // msg = elem.dataset.validationRequired; break; case 'format': msg = $(elem).attr('data-validation-format'); ///without jQuery // msg = elem.dataset.validationFormat; break; default: break; } ///make popup appear/// let ttText = $(elem).parent().children('.ttText'); $(ttText).text(msg); $(ttText).addClass('active'); ///without jQuery // let ttText = elem.parentElement.querySelector('.ttText'); // ttText.innerText = msg; // ttText.classList.add('active'); }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.0/jquery.min.js"></script>
With that styling and custom function, you can apply it to your existing code for any form you are willing to apply to. In this case, I made an example form that takes in a name and email that are required, while the description is not required.
const contactUsForm = document.querySelector('#Form'); const submitBtn = document.querySelector('#submitBtn'); if (contactUsForm) { function customReportValidatity(elem, type) { let msg = ""; ///check if validity is based on required or mismatch/// switch (type) { case 'required': msg = $(elem).attr('data-validation-required'); ///without jQuery // msg = elem.dataset.validationRequired; break; case 'format': msg = $(elem).attr('data-validation-format'); ///without jQuery // msg = elem.dataset.validationFormat; break; default: break; } ///make popup appear/// let ttText = $(elem).parent().children('.ttText'); $(ttText).text(msg); $(ttText).addClass('active'); ///without jQuery // let ttText = elem.parentElement.querySelector('.ttText'); // ttText.innerText = msg; // ttText.classList.add('active'); } function Validate() { let isValid = true; /* RegEx patterns */ const emailPattern = /^((([a-z]|d|[!#$%&'*+-/=?^_`{|}~]|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])+(.([a-z]|d|[!#$%&'*+-/=?^_`{|}~]|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])+)*)|((x22)((((x20|x09)*(x0dx0a))?(x20|x09)+)?(([x01-x08x0bx0cx0e-x1fx7f]|x21|[x23-x5b]|[x5d-x7e]|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])|(\([x01-x09x0bx0cx0d-x7f]|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF]))))*(((x20|x09)*(x0dx0a))?(x20|x09)+)?(x22)))@((([a-z]|d|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])|(([a-z]|d|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])([a-z]|d|-|.|_|~|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])*([a-z]|d|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF]))).)+(([a-z]|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])|(([a-z]|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])([a-z]|d|-|.|_|~|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])*([a-z]|[u00A0-uD7FFuF900-uFDCFuFDF0-uFFEF])))$/i; let validatedFields = contactUsForm.querySelectorAll('[data-validation-required],[data-validation-format]'); validatedFields.forEach(field => { if (field.getAttribute('type') === 'email') { field.setAttribute('pattern', emailPattern); } if (field.validity.valueMissing) { field.setCustomValidity(field.dataset.validationRequired); customReportValidatity(field, 'required'); isValid = false; } else if (field.validity.typeMismatch) { //using typeMismatch instead of patternMismatch because the regex is not working for emails field.setCustomValidity(field.dataset.validationFormat); customReportValidatity(field, 'format'); isValid = false; } contactUsForm.checkValidity(); /// Recheck on field value change /// field.addEventListener('change', function() { $('.ttText').removeClass('active'); ///without jquery /*document.querySelectorAll('.ttText').forEach((tt)=>{ tt.classList.remove('active'); });*/ Validate(); }); }); return isValid; } submitBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); if (Validate()) { // contactUsForm.submit(); //you can use this output to check if the form will submit console.log("Form Submitted!"); } else { return false; } }); }
.ttCont { position: relative; display: inline-block; } .ttCont .ttText { display: inline-block; visibility: hidden; min-width: 200px; background-color: darkblue; color: #fff; text-align: center; border-radius: 6px; padding: 5px; opacity: 0; transition: opacity 0.5s; position: absolute; z-index: 1; top: 5px; left: 105%; } .ttCont .ttText::after { content: " "; position: absolute; top: 50%; right: 100%; margin-top: -5px; border-width: 5px; border-style: solid; border-color: transparent darkblue transparent transparent; } .ttCont .ttText.active { visibility: visible; opacity: 1; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.0/jquery.min.js"></script> <form id="Form"> <div class="ttCont"> <label for="name">Name</label><br/> <input type="text" name="name" required data-validation-required="Name is required!" /><br/> <span class="ttText"></span> </div> <br/><br/> <div class="ttCont"> <label for="email">Email</label><br/> <input type="email" name="email" required data-validation-required="Email is required" data-validation-format="Email must have the format similar to example@email.com!" /><br/> <span class="ttText"></span> </div> <br/><br/> <div class="ttCont"> <label for="desc">Description</label><br/> <input type="text" name="desc" data-validation-required="Description is required!" /><br/> <span class="ttText"></span> </div> <br/><br/> <button type="button" id="submitBtn">Submit</button> </form>
You can read more about how to create and handle custom tooltips here: