Form Validation with Messages Next to Controls
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.
In this use case, messages are associated to the invalid form inputs using aria-describedby (for more info, see Labelling elements using aria-label and aria-labelledby):
- For multiple radio buttons or checkboxes, the message is associated to the surrounding <fieldset>.
- For all other inputs, the message is associated to the input itself.
Working Example
Explanation
If there are any validation messages, the focus is set to the first invalid input: this way, a screen reader will immediately announce the associated message, so the user knows that there is at least one invalid input to be fixed.
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 coloured border, a decent background colour, 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 fixing 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
</head>
<body>
<form>
<div class="control">
<label for="name">Full name</label><input id="name" name="name" type="text">
<p id="name_description" class="error hide">Please enter your name!</p>
</div>
<div class="control">
<label for="biography">Biography</label><textarea id="biography" name="biography" type="text"></textarea>
<p id="biography_description" class="error hide">Please tell us something about your history!</p></div>
<fieldset id="fldSetGender" tabindex="-1">
<legend>Gender</legend>
<div class="control">
<input id="gender_male" name="gender" type="radio" value="male"><label for="gender_male">Male</label>
</div>
<div class="control">
<input id="gender_female" name="gender" type="radio" value="female"><label for="gender_female">Female</label>
</div>
<p id="gender_description" class="error hide">Please tell us your gender!</p></fieldset>
<div class="control">
<input id="accept_agbs" name="accept_agbs" type="checkbox" value="1">
<label for="accept_agbs">I accept the terms and conditions</label>
<p id="accept_agbs_description" class="error hide">You must accept our terms and conditions!</p></div>
<div class="control">
<input name="validate" type="hidden" value="1">
<input type="button" value="Register" onClick="return validateForm()">
</div>
</form>
<style>.control, fieldset {
margin: 6px 0;
}
label {
display: inline-block;
width: 120px;
vertical-align: top;
}
input + label {
width: auto;
}
.error {
color: red;
margin-top: 0;
margin-left: 120px;
}
label + .error {
margin-left: 0;
}
fieldset .error {
margin-left: 0;
}
.hide{
display: none;
}
</style>
<script type="text/javascript">
const validateForm = () => {
const name = document.getElementById("name");
const biography = document.getElementById("biography");
// const message = document.getElementById("message").value;
const gender = document.querySelector(`input[name="gender"]:checked`);
const acceptTerms = document.getElementById("accept_agbs");
let isValid = true, focusEl;
if (name.value.trim() === "") {
isValid = false;
const nameErrDesc= name.nextElementSibling;
name.setAttribute('aria-describedby',nameErrDesc.id);
nameErrDesc.classList.remove('hide');
focusEl=name;
}else name.nextElementSibling.classList.add('hide');
if (biography.value.trim() === "") {
isValid = false;
const bioErrDesc= biography.nextElementSibling;
biography.setAttribute('aria-describedby',bioErrDesc.id);
bioErrDesc.classList.remove('hide');
if(!focusEl) focusEl =biography;
}else biography.nextElementSibling.classList.add('hide');
if (!gender) {
isValid = false;
const FldsetRadio = document.getElementById('fldSetGender');
const genderErrDesc= document.getElementById('gender_description');
FldsetRadio.setAttribute('aria-describedby', genderErrDesc.id);
genderErrDesc.classList.remove('hide');
if(!focusEl) focusEl = FldsetRadio;
} else document.getElementById('gender_description').classList.add('hide');
if(!acceptTerms.checked){
isValid = false;
const acceptTermsErrDesc= document.getElementById('accept_agbs_description');
acceptTerms.setAttribute('aria-describedby',acceptTermsErrDesc.id);
acceptTermsErrDesc.classList.remove('hide');
document.getElementById('accept_agbs_description').classList.remove('hide');
if(!focusEl) focusEl = acceptTerms;
}
if(focusEl) focusEl.focus();
return false;
}
</script>