Form Validation with Messages on Top
Data submitted in a form is usually validated in some way. And if there is any unacceptable data, the form is traditionally re-displayed, together with validation messages. In such a case, it is important to immediately inform screen reader users, so they know that they have to look at their data and submit again.
The following example is very similar to the one above, except in this use case, all messages are displayed inside a <fieldset> / <legend> structure on top of the form. Each message is an in-page link, targeting the respective invalid input.
In addition to this, each invalid input is associated to its message using aria-describedby. This is important, as it makes sure that screen readers also announce the messages when navigating through the inputs.
Working Example
Explanation
If there are any validation messages, the focus is set to the first message: this way, a screen reader will immediately announce it, so the user knows that there is at least one invalid input to be fixed. As the message is also announced as “in-page link”, the user can activate it and jump to the respective input; but the user also may decide to stay in the messages block and read the other messages before fixing any of the inputs.
After fixing the invalid input, the user can search for other invalid ones or simply submit the form again to repeat the process.
Our examples above are very simple and created mainly to demonstrate screen reader usage. Please optimize your own form controls and validations for other
users, too.
- For example, add colours and other visual attributes to invalid fields, for example, a thick colored border, a decent background color, etc.
- Graphical icons can be a useful indicator, too, for example, a fancy exclamation mark.
- It is also important to provide users with meaningful messages that help them fix their input: while “Incorrect input format” is not very helpful for a date input, something like “Please enter in format YYYY/MM/DD” is much better.
Code
<div id="error-container" role="alert" aria-live="polite">
<ul class="error-list"></ul>
</div>
<form id="contactForm" onsubmit="return validateForm()">
<div class="form-control">
<label for="name">Full Name:</label>
<input type="text" id="name" name="name" aria-describedby="nameError">
</div>
<div class="form-control">
<label for="email">Email:</label>
<input type="email" id="email" name="email" aria-describedby="emailError">
</div>
<div class="form-control">
<label for="message">Message:</label>
<textarea id="message" name="message" aria-describedby="messageError"></textarea>
</div>
<div class="form-control">
<fieldset>
<legend>Gender:</legend>
<input type="radio" id="gender-male" name="gender" value="male" aria-describedby="genderError">
<label for="gender-male">Male</label>
<input type="radio" id="gender-female" name="gender" value="female" aria-describedby="genderError">
<label for="gender-female">Female</label>
</fieldset>
</div>
<div class="form-control">
<input type="checkbox" id="accept-terms" name="accept-terms" value="1" aria-describedby="termsError">
<label for="accept-terms">I accept the terms and conditions</label>
</div>
<div class="form-control">
<input type="submit" value="Submit">
</div>
</form>
<script>
function validateForm() {
var name = document.getElementById("name").value;
var email = document.getElementById("email").value;
var message = document.getElementById("message").value;
var gender = document.querySelector('input[name="gender"]:checked');
var acceptTerms = document.getElementById("accept-terms").checked;
var errors = [];
if (name.trim() === "") {
errors.push("name");
}
if (email.trim() === "") {
errors.push("email");
}
if (message.trim() === "") {
errors.push("message");
}
if (!gender) {
errors.push("gender");
}
if (!acceptTerms) {
errors.push("accept-terms");
}
if (errors.length > 0) {
displayErrors(errors);
return false;
}
return true;
}
function displayErrors(errors) {
var errorContainer = document.getElementById("error-container");
var errorList = document.querySelector(".error-list");
errorList.innerHTML = ""; // Clear existing errors
errors.forEach(function (fieldName, index) {
var errorLinkId = "error-link-" + index; // Generate unique IDs for error links
var listItem = document.createElement("li");
var errorLink = document.createElement("a");
errorLink.textContent = "Error in " + fieldName;
errorLink.className = "error-message";
errorLink.href = "#"; // Make it focusable
errorLink.id = errorLinkId; // Set the unique ID
errorLink.addEventListener("click", function (e) {
e.preventDefault();
moveFocusToField(fieldName);
});
listItem.appendChild(errorLink);
errorList.appendChild(listItem);
// Associate the error link with its corresponding form field using aria-describedby
var field = document.querySelector('[name="' + fieldName + '"]');
if (field) {
field.setAttribute('aria-describedby', errorLinkId);
}
});
errorContainer.style.display = "block"; // Show error container
document.getElementById("error-link-0").focus(); // Focus the first error link
}
function moveFocusToField(fieldName) {
var field = document.querySelector('[name="' + fieldName + '"]');
if (field) {
field.focus();
}
}
</script>