While we were building our own accessible components library called Tenon UI, we found that it is surprisingly difficult to accessibly link validation and additional informative or hint texts to checkbox and radiobutton groups.
In this article we will discuss our findings and propose a reliable solution, but first we will start by looking at the standard recommended ARIA method.
TL;DR; Our solution revolves around setting hint and error texts as part of the text of the legend element. You can find a working example on the full form demo page of Tenon UI.
Linking information texts with aria-describedby
For single controls, setting up validation errors in an accessible way is easy enough using aria-describedby:
<label for="nameInput">First name</label>
<input id="nameInput" type="text" aria-required="true" aria-invalid="true" aria-describedby="errorText"/>
<span id="errorText">You have not entered a name</span>
By linking the id of the error text container to the input with aria-describedby, screen reader users will also get the benefit of the extra descriptive validation text to assist in filling out the form correctly.
Also note the use of aria-invalid to indicate the current invalid state to screen reader users. This is the ARIA version of displaying red borders and error icons to users.
Content hint texts can also be linked to controls using aria-describedby:
<label for="nameInput">First name</label>
<input id="nameInput" type="text" aria-required="true" aria-describedby="hintText"/>
<span id="hintText">Please enter only your first name</span>
This method is the recommended way to give users extra information so that completing forms do not become an exercise of discovery as error messages often only fire once the form has been submitted. In the wild, the placeholder text is often used for contextual hints, but this is an anti-pattern. Placeholder texts disappear once the user interacts with the control taking away the benefits by making it cognitively harder.
With aria-describedby one can even combine both hint texts as well as error messages:
<label for="nameInput">First name</label>
<input id="nameInput" type="text" aria-required="true" aria-invalid="true" aria-describedby="hintText errorText"/>
<span id="hintText">Please enter only your first name</span>
<span id="errorText">You have not entered a name</span>
In this way you can decorate all your single controls with hint and error information.
Groups of controls
This easy method unfortunately breaks down completely when we have groups of controls that act as one. Specifically radiobuttons but also groups of checkboxes that describe one concept.
But what validation could we possibly do for these groups? If the form and system as a whole is properly designed it should not be possible to enter incorrect information using these controls.
This is exactly the assumption that seems to have lead to no easy pattern for linking validation or hint texts to these groups. Historically this has been solved by pre-selecting at least one of the controls, but this does not seem to cover every use case.
Think of a group of radiobuttons where we ask the user to vote for a time of day to host a social event. Pre-selecting an option could lead to a very unsuccessful event and unhappy attendees!
So this leaves us with two common cases where validation for groups of controls are a required:
- Required validation where the user is asked to choose at least one option.
- Maximum number of selection where the user is asked to select no more than, say, five options. This is of course specific to a group of checkboxes.
aria-describedby breaks down
Whereas it is easy enough to link multiple messages to one control using aria-describedby, there seems to be no way with ARIA to link one message to a group of controls in a way that is consistently clear to all users.
We tried a number of variations and tested it in a number of screen readers and browsers and this was our findings at the time of the tests:
Attaching aria-describedby to the fieldsets
As we recommend wrapping such control groups in a fieldset and then label the group with a legend, the obvious method seems to be to attach the aria-describedby to the fieldset as well. So we did:
<fieldset aria-describedby="foo">
<legend>Select your favourite colours.</legend>
<input type="checkbox" name="blackInput" id="black" />
<label for="black">Black</label>
<input type="checkbox" name="redInput" id="red" />
<label for="red">Red</label>
</fieldset>
<span id="foo">You must select at least one colour</span>
But how does this perform?
- JAWS/Firefox: Legend, error and label are read.
- JAWS/IE11: Legend is read but no error.
- JAWS/Edge: Only the label is read, not the legend or the error.
- JAWS/Chrome: Legend, error and label are read.
- NVDA/Firefox: Legend is read but no error.
- NVDA/IE11: Legend, error and label are read, at least the first time. Moving back into the fieldset later does not read legend nor description.
- NVDA/Edge: Only the label is read, not the legend or the error.
- NVDA/Chrome: Only the label is read, not the legend or the error.
- VoiceOver/Safari: Legend and label are read twice, error is not read.
- VoiceOver/Chrome: Only the label is read, not the legend or the error.
- Narrator/Edge: Only the label is read, not the legend or the error.
While this does not cover all combinations out there, it does show that we have a pretty unreliable situation. Many users will find themselves confused by this solution.
This came as a bit of a surprise, considering the prolific use of both fieldsets as well as aria-describedby on the web.
Attaching aria-describedby to the legends
Now we were in experimental mode. One could reason that because the legend of a fieldset is reliably read out by screen readers that attaching the error message to the legend itself could perhaps cause the error to be read out as well.
<fieldset>
<legend aria-describedby="foo">Select your favourite colours.</legend>
<input type="checkbox" name="blackInput" id="black" />
<label for="black">Black</label>
<input type="checkbox" name="redInput" id="red" />
<label for="red">Red</label>
</fieldset>
<span id="foo">You must select at least one colour</span>
This experiment lead to unusable results in all the combinations we tried:
- JAWS/Firefox: Only the label is read, not the legend or the error.
- JAWS/IE11: Legend is read but no error.
- JAWS/Edge: Only the label is read, not the legend or the error.
- JAWS/Chrome: Only the label is read, not the legend or the error.
- NVDA/Firefox: Legend is read but no error.
- NVDA/IE11: Only the label is read, not the legend or the error.
- NVDA/Edge: Only the label is read, not the legend or the error.
- NVDA/Chrome: Only the label is read, not the legend or the error.
- VoiceOver/Safari: Legend and label are read twice, error is not read.
- VoiceOver/Chrome: Legend is read but no error.
- Narrator/Edge: Only the label is read, not the legend or the error.
Setting the error on the first input
Admittedly we had little hope for this one, but we attempted using the first control in the group for the attachment point for both aria-describedby as well as aria-invalid.
The reason we attempted this was that linking the information to every control in the group would be terrible for the user. If every control says that it is invalid while the group is invalid it would lead to very verbose repetition for screen reader users while leading to a confusing state in checkbox groups where a selected control can be invalid just because the minimum selected number has not been reached.
The slim hope was that because it is the first control in the group the user would encounter it seldom enough for the solution to work. We also tested what the effect would be to navigate to the error control from some in-page error link:
<a href="#black">You must select at least one colour!</a>
<fieldset>
<legend>Select your favourite colours.</legend>
<input type="checkbox" name="blackInput" id="black" aria-invalid="true" aria-describedby="foo" />
<label for="black">Black</label>
<input type="checkbox" name="redInput" id="red" />
<label for="red">Red</label>
</fieldset>
<span id="foo">You must select at least one colour</span>
Surprisingly, this lead to a much better experience than our test with the legend as illustrated above.
- JAWS/Firefox: When tabbing from link, only the legend and for some reason the page title is heard. Must move between radios (or checkboxes) to hear the error and labels.
- JAWS/IE11: Legend, error and label are read.
- JAWS/Edge: Only the label is read, not the legend or the error.
- JAWS/Chrome: Legend, error and label are read, though repeats somewhat.
- NVDA/Firefox: When navigating from an error link, reads only the unchecked and aria-invalid states. Only moving deliberately into forms mode (tabbing for instance) and back to the first input is the label and error message read (no legend even then).
- NVDA/IE11: When navigating from an error link, reads only the unchecked and aria-invalid states. Only moving deliberately into forms mode (tabbing for instance) and back to the first input is the label and error message read (no legend even then).
- NVDA/Edge: Legend, error and label are read.
- NVDA/Chrome: Legend, error and label are read.
- VoiceOver/Safari: Legend and label are read twice, error is not read.
- VoiceOver/Chrome: Legend, error and label are read.
- Narrator/Edge: Label, aria-invalid state, and description is read, but no legend.
Our hope paid off more than expected! However, the results still did not inspire the confidence we wanted.
Dynamically injecting the error text into the legend – Our solution
Our winning combination came from completely stepping away from the tried and tested aria-describedby and manipulating the legend text based on the current validity state of the control. When an error occurred we appended it to the text in the legend and removed it again if no error condition existed.
<fieldset>
<legend>
<span>Select your favourite colours.</span>
<span>You must select at least one colour!</span>
</legend>
<input type="checkbox" name="blackInput" id="black"/>
<label for="black">Black</label>
<input type="checkbox" name="redInput" id="red" />
<label for="red">Red</label>
</fieldset>
The result was pretty reliable:
- JAWS/Firefox: Legend, error and label are read.
- JAWS/IE11: Legend, error and label are read.
- JAWS/Edge: Only the label is read, not the legend or the error.
- JAWS/Chrome: Legend, error and label are read.
- NVDA/Firefox: Legend, error and label are read.
- NVDA/IE11: Legend, error and label are read.
- NVDA/Edge: Legend is only read at first. After a selection is made, no legend is ever read again.
- NVDA/Chrome: Legend, error and label are read.
- VoiceOver/Safari: Legend, error and label are read twice.
- VoiceOver/Chrome: Legend, error and label are read.
- Narrator/Edge: Label is read but no legend, therefore also no error message.
With these results we were confident enough to recommend it as the the most reliable method found and tested.
We believe the double announcement with VoiceOver in Safari to be a bug and will be submitting it.
Note: This pattern is applied in exactly the same way to a group of radiobuttons.
Why not use the radiogroup role?
We did attempt using the radiogroup role, but there is no equivalent option for groups of checkboxes.
We also found similar problems with a container marked with the radiogroup role with either the legend being read out twice or the error message not being read out at all.
And the hint texts?
For simplicity, the code above mainly demonstrates attaching error text, however the same technique can be used for content hint texts.
What about aria-invalid?
As previously mentioned, setting aria-invalid on each control will lead to a confusing user experience. So we chose to forgo using this ARIA state on the control groups.
Clear content hint and error texts should be more than sufficient to notify the user that the group is not valid.
And finally… focus
An important ingredient for complex accessible forms is displaying a summary of error fields at the top of the form and allowing the user to navigate quickly to the invalid controls to fix the issues faster. This is done by creating in-page links to the error controls.
But what to do in the case of groups of controls?
When we attempted to set focus to the group container elements themselves, we once again had substandard results. So our recommended solution is to set focus to the first element in the group. In combination with our solution for hint and error texts this yielded the best result as well as the most efficient keyboard navigation pattern:
<a href="#black">You must select at least one colour!</a>
<fieldset>
<legend>
<span>Select your favourite colours.</span>
<span>You must select at least one colour!</span>
</legend>
<input type="checkbox" name="blackInput" id="black"/>
<label for="black">Black</label>
<input type="checkbox" name="redInput" id="red" />
<label for="red">Red</label>
</fieldset>
Summary – The working example
Head over to the full form demo page of Tenon UI to see this in action.
We are curious what you think about this pattern. We are also curious how others have solved this. Please let us know!
We appreciate any discussions in the forms of issues or pull requests on the GitHub repository of Tenon UI.
Versions used for testing
- JAWS version: 2018.1808.10 ILM
- NVDA version: 2018.3
- Firefox version: 62
- Internet Explorer 11
- Edge version: 42.17134.10/EdgeHTML 17
- Chrome 68.0.3443.106
- Narrator, version unknown
- Safari: 12.0.1
- VoiceOver on macOS Mojave 10.14.1