In this article you’ll learn how to use ARIA and Vue.js to make your form error messages and instructions more accessible to all of your users.
Any developer who is building forms with accessibility in mind needs to understand some important differences between how a sighted user and a screen reader user “reads” web forms. The first is that there is a “hidden” dimension of your web page known as the “accessibility tree”. The accessibility tree is a DOM-like structure that enable the screenreader get information from your browser.
Using ARIA, the developer can customize certain aspects of the page to enhance the accessibility of your content and overall experience for screenreader users.
A second difference is that the two most popular screenreaders use (or can use) a specific mode known as “forms” or “focus” mode to navigate web forms more easily. While in forms mode, the screenreader enables the user to navigate among the form’s interactive controls with the keyboard. When the focus arrives at a control, the screenreader reads both the input and and the associated label. That’s pretty slick, isn’t it?
Forms mode sounds pretty useful — but what about the other items that we often add to forms, such as validation error messages, or instructions we might want to provide for fields (hints for required formats, acceptable or required characters, etc.). If the developer places these within an element that is not inherently focusable, such as a <div>
or <p>
, a screenreader in forms mode will skip right over them. That’s not what we want! So how do we ensure that screenreader will read this additional (and ofen essential) information?
By far the easiest way to make your validation error messages accessible is to make them a child of the <label>
element. By making the error message part of the label, the message becomes part of the accessible name for the control — in this case, the input element – and will be read when ever the control has focus. Of course, you will want to use Vue’s v-show directive to hide the error message until there is a validation error. Since it utilizes CSS’s display: none, v-show will also keep the error message out of the accessibility tree, which hides it from screen readers.
For some, this can be a quick and easy solution. It doesn’t require any ARIA, keeps your code simple, and minimizes the possibilities for coding errors.
But what if you would rather not have your error messages within the <label>
element? There might be good reasons for this. For example, tampering with the control’s accessible name when there is an error might seem like a kludge. Or perhaps you want to place the messages someplace else relative to the control, or even display them within their own list or block element (both of which are invalid inside <label>
).
If you prefer to keep accessible name “pure”, or want more flexibility, you can still make your error messages accessible. ARIA (“Accessible Rich Internet Applications”) attributes enable developers to add semantics where HTML alone isn’t sufficient. ARIA attributes have no effect on a sighted user’s web experience. But they do affect how screen readers interpret a page through the accessibility tree.
As luck would have it, ARIA provides an attribute that enables developers to associate other html elements with a form field: aria-describedby
. To provide field instructions, simply add the aria-describedby
attribute to the field input with the id
of each element you want to link with the input
. The id
s should be space-separated and entered in the order you want them read.
<label for="first_name">First Name:</label>
<input id="first_name" type="text" aria-describedby="first_name-instructions">
<div id="first_name-instructions">maximum length 30 characters</div>
Wen the focus is placed on the <input>
, the screenreader will say something like this:
“First name colon edit. maximum length 30 characters edit”
Now that we have explicitly associated additional instructions with our field, we also want to add an error messages. Let’s try this:
<div id="first_name-error">
Please enter a valid project name.
</div>
<label for="first_name">First Name:</label>
<div id="first_name-instructions">maximum length 30 characters</div>
<input id="first_name" name="first_name" type="text" aria-describedby="first_name-instructions first_name-error">
And with one simple attribute, we have added an error message and associated it with the form input.
But we’re not done yet. First, we don’t want the error message to be displayed and read all of the time; we only want to see or hear it when there is an error. This example uses the excellent Vuelidate library.
<div id="first_name-error" v-show="first_name.$error">
Please enter a valid project name.
</div>
<label for="first_name">First Name:</label>
<div id="first_name-instructions">maximum length 30 characters</div>
<input id="first_name" name="first_name" type="text" v-model="$v.first_name.$model" :aria-invalid="$v.first_name.$invalid" aria-describedby="first_name-instructions first_name-error">
Now we have an error message that is associated with the field input, but will be visually hidden unless a validation error is detected. Since we are using v-show
, we might expect the message to be hidden from screen readers as well, and under most circumstances, it will. But here we encounter a feature of aria-describedby
that might seem counter-intuititive: by default, it will read a referenced element even when that element is hidden. (it makes no difference whether this is done by css or aria-hidden). To make our solution work as intended, we need to make aria-describedby
dynamic so that it adds the id for the error message only when therre is an error. Of course Vue.js makes this quite easy. Have a look at this example:
signup-form.html
<div id="first_name-error" v-show="first_name.$error">
Please enter a valid first name
</div>
<label for="first_name">First Name:</label>
<div id="first_name-instructions">maximum length 30 characters</div>
<input id="first_name" name="first_name" type="text" v-model="$v.first_name.$model" :aria-invalid="$v.first_name.$invalid" :aria-describedby="describedBy('first_name')">
main.js
methods: {
// generate aria-describedby reference ids
describedBy(field) {
const inst = `${field}-instructions`
// field.$error is a boolean computed property returned by Vuelidate
// if there is an error, valErr becomes the field id. Otherwise it is an empty string.
const valErr = field.$error
? `${field}-error`
: ''
//trim and replace double-spaces with single space
let refString = ` $ {valErr} ${inst}`.replace(/\s+/g,' ').trim()
return refString
}
// a basic error message builder
vMessage(v, field) {
let message = ''
let errors = []
if ($v.$error)) {
// get error types from Vuelidate $params
let errorTypeKeys = Object.keys($v["$params"])
// build errors array
for (const key of errorTypeKeys) {
if ($v[key] === false) {
errors.push(key)
}
}
//build comma=separated string from array
let errorString = errors.length > 1
? errors.join(', ')
: errors[0]
// convert to more readable message
errorString = errorString
.replace('required', 'This is a required field')
.replace('url', 'The url is invalid')
.replace('email', 'The email address is invalid')
.replace('minLength', 'Input does not meet minimum length')
message = `${errorString}.`
}
return messsage
}
}
Now we have a dynamic aria-describedby
attribute that is bound to the output of the describedBy()
method. describedBy()
takes the field name as a parameter; determines whether the field input is valid; and returns the appropriate string of space-separated list of ids. If there is an error, and the focus is placed on the <input>
, aria-describedby
will reference both the error message and the instructions, and the screen reader will announce both. Otherwise, the screen reader will announce only the instructions (the <label>
will be announced regardless).
Developers should be aware that, as with web browsers, screen readers are not all the same. They can interpret html or ARIA in their own unique way, have their own feature sets, and their functionality can vary when used with different web browsers. For example, both JAWS and NVDA support both forms (focus) mode and aria-describedby, while Voiceover supports aria-describedby, but it does not have a focus or forms mode. NVDA seems to work most reliably with Firefox, while Voiceover seems to work best with Safari.
While there is broad support for aria-describedby
among screen readers and (modern) web browsers, it does have some behaviors that developers should be aware of. For example, in addition to reading hidden referenced elements (above), aria-describedby
seems to disregard semantics; it reads referenced elements as a continuous string. If your instructions and messages contain lists or other nested elements, the semantics will be ignored, and in some cases, the content might not be read at all. Therefore, it is best to keep message content short and simple, and use punctuation. For a complete list of caveats, see the Scott O’Hara article cited at the end of this article.
Using aria-describedby for validation error messages might not seem like an especially elegant solution. Of course, ARIA is still relatively young. In late-2017, ARIA 1.1 added the aria-errormessage
attribute, which is intended to deal specifically with validation error messages. When it gains support in screen readers and browsers, aria-errormessage
will be used together with the aria-invalid
attribute to provide a more coherent method for reading out the error message. But as of this writing, support for aria-errormessage
is still poor to nonexistent, so for now, developers should user aria-describedby
to make form field instructions and errors more accessible.
All of the above should make it clear that neither automated tools nor visually viewing the site yourself can tell you whether your forms are working as intended, and are providing an inclusive experience for all users. The only way to ensure this is by testing with a screen reader. So fire up a copy of NVDA, Voiceover (both free), or JAWS (if you can afford it), get under “the hood” and start exploring the non-visual dimension of the web. You may be surprised by what you hear — and discover.
Recommended Reading
☞ How to use PWA plugin in Vue CLI 3.0
☞ How to Using Cypress with Django and Vue for integration testing in GitLab CI
☞ Going Serverless with Vue.js
☞ What React Hooks Mean for Vue developers
☞ Build a Single-Page App with Go and Vue
☞ Server Side Pagination with Vue.js and Node
☞ Build Your First PWA with Vue and TypeScript
#vue-js #javascript