Remember this autocomplete component we’ve built? Although most users are able to use it, people with disabilities that require an assistive technology to browse the web won’t. That is because we didn’t make it semantic enough for these technologies to understand that our component is more than a regular input.
In this article were are going to learn how to use ARIA attributes to make our autocomplete into a fully accessible one.
Have you ever tried to browse the web with an assistive technology? Most operative systems come with an integrated solution, in MacOS you can open VoiceOver, by pressing cmd + F5
and on Windows you can start Narrator by pressing Windows logo key + Ctrl + Enter
.
When we use one of the above with this autocomplete component it will tell us that the autocomplete is a text field and won’t inform us about the list of options.
We can change that with the help of ARIA attributes. The ARIA specification defines how to make the web content usable by people with disabilities by providing a set of attributes that allows assistive technology softwares to understand the semantics of the content.
You would be surprised on how much a simple label can improve usability.
Let’s quickly setup our component into an application and use VoiceOver to interact with it.
<template>
<div id="app">
<div>
<label>Choose a fruit:</label>
<autocomplete
:items="[ 'Apple', 'Banana', 'Orange', 'Mango', 'Pear', 'Peach', 'Grape', 'Tangerine', 'Pineapple']"
/>
</div>
</div>
</template>
When we enable VoiceOver to interact with our component we are only aware of the presence of a textfield but we have no idea for what it is for since the label isn’t picked up by the assistive technology.
By adding either aria-label
or aria-labelledby
attributes we’ll enable the user to know for what this input is for.
Let’s add a prop to our autocomplete for the aria-labelledby
attribute. Note that you can choose to provide the aria-label
instead, but because most autocomplete components have a label element nearby, I’m going to take advantage of that:
<script>
export default {
...
props {
...
ariaLabelledBy: {
type: String,
required: true,
},
};
};
</script>
<template>
...
<input
type="text"
v-model="search"
@input="onChange"
:aria-labelledby="ariaLabelledBy"
/>
...
</template>
I’ve made it a required attribute to make sure no one ever forgets to add it. If your application won’t ever have a label element surrounding the component it may be wiser to use the aria-label
attribute instead.
We just need to add an id
to our label and to provide it as a prop:
<template>
<div id="app">
<div>
<label id="fruitLabel">Choose a fruit:</label>
<autocomplete
:items="[ 'Apple', 'Banana', 'Orange', 'Mango', 'Pear', 'Peach', 'Grape', 'Tangerine', 'Pineapple']"
aria-labelled-by="fruitlabel"
/>
</div>
</div>
</template>
And now the assistive technology is able to tell us that the input text intent is to choose a fruit:
Although labels can improve the usability tremendously, they are not enough, the user still doesn’t know it’s an autocomplete element. In order to do so we need to use other ARIA attributes.
Let’s start by understanding how the role
attribute works.
Roles define the element type of the element. In here you can check all different types of roles.
The more suitable one for our autocomplete is the combobox
one:
A composite widget containing a single-line textbox and another element, such as a listbox or grid, that can dynamically pop up to help the user set the value of the textbox.
Because the input of text in our component will display a list of results for the intended value we also need to set the aria-autocomplete
attribute in the textbox element.
The aria-autocomplete
attribute allows three different values, an inline
value which defines that the value completion will happen inside the text input and a list
value which means the values will be present in a separate element that pops up adjacent to the text input or a both
value which means that a list of values will be displayed and when displayed one value in the list is automatically selected and will be visible inside the text input.
Because our list of options is in a separate element, we’ll need to use the list
value.
This attribute alone doesn’t magically know where our list of values is in the document, so we need to specify that by using the aria-controls
attribute.
We also need to ensure our autocomplete is identified with the aria-haspopup
attribute and that our container has a aria-expanded
attribute set whenever the list of results is visible.
Last but not least, we also need to add the role
attribute to our input
with the searchbox
value, to the ul
element with listbox
and to each li
with role
value.
With these attributes the assistive technology software is now able to understand we are presenting the user with a combobox that will show a list of suggested values.
<template>
<div
class="autocomplete"
role="combobox"
aria-haspopup="listbox"
aria-owns="autocomplete-results"
:aria-expanded="isOpen"
>
<input
type="text"
@input="onChange"
v-model="search"
@keyup.down="onArrowDown" @keyup.up="onArrowUp" @keyup.enter="onEnter" aria-multiline="false"
role="searchbox"
aria-autocomplete="list"
aria-controls="autocomplete-results"
aria-activedescendant=""
:aria-labelledby="ariaLabelledBy"
/>
<ul
id="autocomplete-results"
v-show="isOpen"
class="autocomplete-results"
role="listbox"
>
<li class="loading" v-if="isLoading">
Loading results...
</li>
<li
v-else
v-for="(result, i) in results"
:key="i"
@click="setResult(result)" class="autocomplete-result"
:class="{ 'is-active': i === arrowCounter }"
role="option"
>
{{ result }}
</li>
</ul>
</div>
</template>
Remember that we added keyboard support in our autocomplete component? We need to manage it with ARIA attributes too.
In order for the assistive technology to know which option is selected when we use the arrow keys, we’ll need to set two attributes:
The aria-activedescendant
needs to be set in the input field and it will hold the ID of the option which is visually identified as having keyboard focus.
And the aria-selected
one needs to be set in the li
attribute in the option visually highlighted as selected.
One important thing we need to update in our component are the listeners, in order for the assistive technology to correctly identify which option is active, we need to listen to the keydown
event instead of the keyup
event.
You can see the full source code in the following snippet or in this codepen.
<script>
export default {
name: 'autocomplete',
props: {
items: {
type: Array,
required: false,
default: () => [],
},
isAsync: {
type: Boolean,
required: false,
default: false,
},
ariaLabelledBy: {
type: String,
required: true
}
},
data() {
return {
isOpen: false,
results: [],
search: '',
isLoading: false,
arrowCounter: 0,
activedescendant: ''
};
},
methods: {
onChange() {
this.$emit('input', this.search);
if (this.isAsync) {
this.isLoading = true;
} else {
this.filterResults();
}
},
filterResults() {
this.results = this.items.filter((item) => {
return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1;
});
},
setResult(result) {
this.search = result;
this.isOpen = false;
},
onArrowDown(evt) {
if (this.isOpen) {
if (this.arrowCounter < this.results.length) {
this.arrowCounter = this.arrowCounter + 1;
this.setActiveDescendent();
}
}
},
onArrowUp() {
if (this.isOpen) {
if (this.arrowCounter > 0) {
this.arrowCounter = this.arrowCounter -1;
this.setActiveDescendent();
}
}
},
onEnter() {
this.search = this.results[this.arrowCounter];
this.arrowCounter = -1;
},
handleClickOutside(evt) {
if (!this.$el.contains(evt.target)) {
this.isOpen = false;
this.arrowCounter = -1;
}
},
setActiveDescendant() {
this.activedescendant = this.getId(this.arrowCounter);
},
getId(index) {
return `result-item-${index}`;
},
isSelected(i) {
return i === this.arrowCounter;
},
},
watch: {
items: function (val, oldValue) {
// actually compare them
if (val.length !== oldValue.length) {
this.results = val;
this.isLoading = false;
}
},
},
mounted() {
document.addEventListener('click', this.handleClickOutside)
},
destroyed() {
document.removeEventListener('click', this.handleClickOutside)
}
};
</script>
</script>
<template>
<div
class="autocomplete"
role="combobox"
aria-haspopup="listbox"
aria-owns="autocomplete-results"
:aria-expanded="isOpen"
>
<input
type="text"
@input="onChange"
@focus="onFocus"
v-model="search"
@keydown.down="onArrowDown"
@keydown.up="onArrowUp"
@keydown.enter="onEnter"
role="searchbox"
aria-autocomplete="list"
aria-controls="autocomplete-results"
:aria-labelledby="ariaLabelledBy"
:aria-activedescendant="activedescendant"
/>
<ul
id="autocomplete-results"
v-show="isOpen"
class="autocomplete-results"
role="listbox"
>
<li
class="loading"
v-if="isLoading"
>
Loading results...
</li>
<li
v-else
v-for="(result, i) in results"
:key="i"
@click="setResult(result)"
class="autocomplete-result"
:class="{ 'is-active': isSelected(i) }"
role="option"
:id="getId(i)"
:aria-selected="isSelected(i)"
>
{{ result }}
</li>
</ul>
</div>
</template>
Here you can find a cheatsheet with all the ARIA attributes you’ll need to make an autocomplete accessible.
Element | Attribute | Value | Usage |
---|---|---|---|
div |
role |
combobox |
Identifies the element as a combobox |
div |
aria-haspopup |
listbox |
Identifies that the element will popup a lisbox with the suggested values |
div |
aria-owns |
IDREF |
Identifies the element with the suggested list values |
div |
aria-expanded |
true |
Indicates whether the list of suggested values is currently expanded or collapsed |
input |
role |
searchbox |
Identifies the element as a searchbox |
input |
aria-labelledby |
IDREF |
Provides a label for the combobox element |
input |
aria-autocomplete |
list |
Indicates that when a user is providing input an element containing a list of suggested values will be displayed |
input |
aria-controls |
IDREF |
|
input |
aria-activedescendant |
IDREF |
When an option in the list of results is visually identified as having keyboard focus, it will refer to that option |
ul |
role |
listbox |
Identifies the element as a listbox |
li |
role |
option |
Identifies the element as a listbox option |
li |
aria-selected |
true |
Identifies the element as being visually idenfied as selected |
IDREF: A reference to an element’s ID attribute
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!