Creating Accessible Combo Boxes

In this post, we will discuss how to build an inclusive, keyboard, and screen-reader friendly combo box. 

We will cover: 

  1. Defining element semantics 
  2. Defining element relationships
  3. Styling for accessibility
  4. Managing states and values for screen readers

Since we will focus on the accessibility aspects, we will not cover the entire example code. If you want to follow along, you can find the full code for this post’s example on this Github gist and this live demo on CodePen.

What is a combo box, and what accessibility challenges does it entail?

Combo box, as its name implies, is usually made up of a combination of two or more widgets. Traditionally, it is a combination of a drop-down list and a single-line editable textbox, allowing the user to either type a value directly or select an option from the list. Sometimes the term “combo box” is used to indicate “drop-down/select list” (e.g. <select>) since these two widgets have a lot in common, in this post we are going refer to the first type.

Whether you want to build a styled select-list or a classic combo box, the accessibility challenges are mostly similar. The WAI (The W3C’s Web Accessibility Initiative) defines the accessibility requirements of the combo box design pattern on WAI-ARIA Authoring Practices 1.1 and covers various types of combo boxes, including select-lists.

To begin, let’s see what we want to achieve and what are the accessibility challenges this design pattern produce:

Combobox anatomy, general view
Combobox anatomy, general view

Naming and labeling

Combo box is a UI element that requires user input, therefore it must have an accessible name to meet the WCAG’s “3.3.2 Labels or Instructions” and “4.1.2 Name, Role, Value” success criteria.

Unlike native HTML elements that require user input, it is made up of several different HTML elements, with different roles and semantics. Which of the parts that make up the combo box should be labeled? The accessible name is also meant to help with the “operating instructions” of UI elements, so how do you name a UI element that consists of several elements so that the function of each part is clear  – either individually or in the larger context? 

You can read more about accessible names and their roles on the Evinced knowledge base.

Relationships

As already mentioned, a combo box is made up of a few different HTML elements. User interaction with each element may affect the state of other elements. For example, typing into the input field will change the visibility and content of the select list; and clicking the button is toggling the list visibility. This contextual relationship between the different combo box components should be conveyed programmatically so that screen readers can read them clearly and correctly.

Widget states

The combo box select-list usually consists of a list element (<ul>, <ol>) or a collection of <div>s, which doesn’t have “built-in” state attributes. How can we therefore convey the following to the user correctly: the different combobox states, whether the select list is expanded, which item is selected etc.? Also, what do assistive technologies need to get this information?

Handling keyboard navigation

Certain behaviors are expected from a combo box, for example letting the user navigate up and down the select-list using the up and down arrow keys, or selecting an item by clicking the Enter key and other, when it is highlighted. While these behaviors apply to native HTML elements like <select> elements (which we are actually trying to mimic), on our combo box, we will have to handle it programmatically with Javascript and also override some of the keys’ default behaviors.

The markup 

Let’s begin with the markup. We will start with the general construction, and then we will add the attributes that are used by the accessibility APIs that are required for assistive technologies to read it correctly. We will explain the role of each one.

<div class="combo-wrapper">
    <div class="combo-controls">
        <input type="text" />
        <button >▾</button>
    </div>
    <ul>
        <!-- The list items will be added dynamically -->
    </ul>
</div>
Combobox anatomy, general view with its corresponding class names

Now that we have the basic structure, let’s start taking care of the accessibility matters by semantically defining the component as a combo box. The common user can identify the role and purpose of UI elements by their layout and appearance. Screen readers, on the other hand, do not have the ability to parse visual data and therefore they are depended on the semantics of the DOM to read it to the user correctly (e.g. using <button> tags for buttons, <a> tags for links, etc.), only there is no semantic HTML combo box element. There are quite a few commonly used UI components such as tabs, pop-up modals and menu-bars that do not have equivalent HTML element/s. WAI identified this gap and added to the WAI-ARIA roles model a list of common UI component roles to be used and parsed by the accessibility APIs. Combo box is among them.

Applying semantic roles

<div class="combo-wrapper">
    <div class="combo-controls">
        <input role="combobox" type="text" />
        <button> ▾ </button>
    </div>
    <ul role="listbox">
         <!-- The list items will be added dynamically -->
    </ul>
</div>

We can therefore add to the <input> element role=”combobox” attribute, but there is an important note about this choice: In almost every other case I would not recommend adding a different semantic role to an element that already has semantic meaning. It is considered to be a bad practice since the role attribute is meant to apply semantics to non-semantic elements (e.g. <div>, <span>) or to extend the semantics of an element, for example like applying role=”switch” to a checkbox element. Nevertheless, after testing other options, like adding the role=”combobox” to the <div class="combo-controls"> as the WAI suggests, I realized that the only way the screen readers I checked announced the combo box correctly, and not ignore it, was when it was added to the <input> element, and since the input element is part of the widget, the semantics do not contradict (I have tested Mac OS VoiceOver with Chrome, FireFox and Safari and NVDA with Chrome. FireFox and Edge).

To the list element we will add role=”listbox”. This will allow screen readers to refer to the list element as the listbox of <select> elements, that is to say a list with selectable items.

Labeling

The next step is to label the combo box. We would like to first label the whole combo box, for that purpose we can use the <label> element and link it to the input element. The reason we are linking it precisely to this element is that it is the first element of the combo box screen readers read (they skip the <div class=”combo-controls”> element because it does not contain any text nodes. This is also why we had to add the role attribute to the input element). Besides that, when a label element is linked to an input element, clicking on it will set focus on the input, a behavior that serves us well, also with a combo box.

Now we would like to associate the listbox with the same <label> element. Here we have 2 problems: 1. Each <label> elements “for” attribute can be linked only to 1 element. 2. Only form elements should be linked to <label> elements. For such cases, the WAI introduces the aria-labelledby attribute, this attribute allows you to use any element that contains a text node as a labeling element by referring to its id.

The last step is to label the button, and we can use the same approach. Thus we also produce a logical connection between all the elements that make up the combo box.

<label for="combo-input" id="combo-label">
    Combobox Name
</label>
<div class="combo-wrapper">
    <div class="combo-controls">
        <input id="combo-input"  role="combobox" type="text" />
        <button aria-labelledby="combo-label"> ▾ </button>
    </div>
    <ul aria-labelledby="combo-label" role="listbox">
        <!-- The list items will be added dynamically -->
    </ul>
</div>

Setting relationships
We can help screen readers read the component in a more accurate and user-friendly way by noting the relationships between the various elements. We can do so by adding the “aria-controls” attribute to the <input> and <button> elements and linking it to the list element to signify that these controls are controlling the visibility of the list-box.

<label for="combo-input" id="combo-label">
    Combobox Name
</label>
<div class="combo-wrapper">
    <div class="combo-controls">
        <input
            aria-controls="combo-listbox"
            id="combo-input" role="combobox"
            type="text" />
        <button 
            aria-controls="combo-listbox"
            aria-labelledby="combo-label">
             ▾
        </button>
    </div>
    <ul 
        aria-labelledby="combo-label"
        id="combo-listbox"
        role="listbox">
           <!-- The list items will be added dynamically -->
    </ul>
</div>

A note regarding the “aria-controls” attribute: this attribute is supported by only one screen reader, namely JAWS. Nevertheless, in my opinion, adding an attribute is only a small effort considering that, even though JAWS surpassed the premiere to NVDA in popularity according to the 2019 WebAIM’s screen readers survey, it is still reported to be the primary screen reader of 40.1% of desktop screen reader users and this is not a negligible amount of users.

State attributes
In order to indicate to the user whether the list-box is expanded or collapsed, we will add the aria-expanded attribute to the input element with an initial value of “false”. We will programmatically update it later, when the state is changing.

Even though it is not common and allegedly belongs to another accessibility design pattern (menu button), I think it is a good idea to also add the aria-expanded to the button element since it also affects the list-box’s state.

<label for="combo-input" id="combo-label">
    Combobox Name
</label>
<div class="combo-wrapper">
    <div class="combo-controls">
        <input
            aria-controls="combo-listbox"
            aria-expanded="false"
            id="combo-input"
            role="combobox"
            type="text" />
        <button
            aria-controls="combo-listbox"
            aria-expanded="false"
            aria-labelledby="combo-label">
            ▾
        </button>
    </div>
    <ul aria-labelledby="combo-label" id="combo-listbox" role="listbox">
        <!-- The list items will be added dynamically -->
    </ul>
</div>

Additional attributes
There are a few more attributes we can add to provide an inclusive as possible user experience. First, we want to add an aria-haspopup=”listbox” attribute to the input and button elements to indicate that they are triggering the appearance of a list-box. Secondly, we would like to add the aria-autocomplete attribute, which should indicate to the user the type of autocompletion that’s applied. It can take one of three values: “list”, “inline” or “both”. To be honest, none of the screen readers I have tested supported this attribute, nor could I find any documentation about its support. However, it is defined as a required attribute on the WAI-ARIA’s combo box design pattern description so we are going to add it anyway.

If the combo box provides autocompletion behavior for the text input as described in aria-autocomplete, authors MUST set aria-autocomplete on the textbox element to the value that corresponds to the provided behavior.

We will start with a “list” value.

<label for="combo-input" id="combo-label">
    Combobox Name
</label>
<div class="combo-wrapper">
    <div class="combo-controls">
        <input
            aria-autocomplete="list"
            aria-controls="combo-listbox"
            aria-expanded="false"
            aria-haspopup="listbox"
            id="combo-input"
            role="combobox"
            type="text" />
        <button
            aria-controls="combo-listbox"
            aria-expanded="false"
            aria-haspopup="listbox"
            aria-labelledby="combo-label"
            tabindex="-1">
            ▾
        </button>
    </div>
    <ul aria-labelledby="combo-label" id="combo-listbox" role="listbox">
        <!-- The list items will be added dynamically -->
    </ul>
</div>

Now that the markup is set up for now, we still need to handle the list items that are going to populate the list-box. We are going to do that via Javascript.

CSS

When it comes to styling, there are three things we have to take into consideration from the accessibility point of view:

  1. The input[type=”text”] element must have a discernible focus indicator.
  2. The list items must have a discernible “active/selected” indicator.
  3. When the list-box is collapsed, it has to be hidden with “display:none;” so screen readers will ignore it.

Beyond that, it’s all design choices, while we should obviously make sure that there is a sufficient color contrast and all the texts are readable.

Behaviours (JS) 

The essence of this post is accessibility and not building UI components. I will therefore not go into details here about each of the methods, but rather deal with the aspects that affect the overall inclusiveness of the user experience. 

The topics we will cover are:

  1. Mapping the keyboard events and their outcomes.
  2. Single list-items.
  3. Managing the combo box states.

Just for the sake of context, I will say that the Javascript part of our example is based on a “Combobox” object. Onse creating an “instance” of the Combobox object it should be initiated by the  “init” method that’s taking as an argument an object with the following keys:

  • input: should be assigned to point the <input> element
  • list: should be assigned to point the list (<ul>) element
  • listToggleBtn (optional): should be assigned to point the <button> element
  • data: is the data that’s required to populate the list items. It is expected to be an array of objects
  • searchTerm: Should be the key in the data object by which the filtering will be performed
  • listItemElement: A function that returns HTML list-items (we will discuss this function later in this post)

Keyboard events

Let’s start by mapping the keyboard events that we will handle. One of the key principles in building accessible components is to provide all users with the best possible instinctive/natural experience. The combo box, for example, is expected to behave similarly to a <select> list in most aspects, so this is the behavior we wish to provide. It is, therefore, a good idea to start in the mapping of the relevant keyboard events and their desired outcomes, as you can see in the table below. The spine of this mapping, like the rest of the parts of this example, is also based on the WAI-ARIA’s recommended combo box design pattern.

This events table assumes that the DOM focus is always on the text input, even when the visual focus moves to the list items.

KeyFunction
Down ArrowIf the listbox collapsed, then expand the listbox and move the focus to the first list item.
If the listbox is expanded:
No list item selected: Move the focus to the first list item.
If aria-autocomplete’s value is “inline” or “both”, then move the focus to the second list item since the first item is automatically selected.
A list item is selected: Moves the focus to the next list item.
If the focus is on the last list item, move the focus to the first one.
Up ArrowIf the listbox collapsed, then expand the listbox and move the focus to the last list item.
If the listbox is expanded:No list item selected, Move the focus to the last list item.
A list item is selected: Move the focus to the previous list item.
If the focus is on the first list item, move the focus to the last one.
EnterIf the listbox is collapsed, it does nothing.
If the listbox is expanded and one of the list items is selected:
Set the value of the list item to the text box.
Close the list box.
Set the focus to the text box
EscapeClears the textbox.If the listbox is expanded, close it.
HomeMoves the focus to the textbox and places the editing cursor at the beginning of the field.
EndMoves focus to the textbox and places the editing cursor at the end of the field.
TabIf the listbox is expanded, collapse it.
Keyboard events

The listbox items

You are probably aware that we still have a missing part on our markup, the combo box’s list items, which, as mentioned earlier, we are going to add with Javascript.

For this example I chose to create the list items using an external function that is passed as a parameter to each combo box instans, so the combo box mechanism itself can be reused with various list items’ structures. However, the specific implementation of a list item itself is less important to our case than how we are making it accessible in the combo box context.

Let’s start by defining the list-item. 

function comboListElement(itemData, i) {
   
    let resultItem = document.createElement('li');
    resultItem.id = `result-item-${i}`;
    resultItem.setAttribute('role', 'option');
    resultItem.setAttribute('aria-selected', 'false');

    return resultItem;
}

After creating the list element, we will want to add to it a unique id value, because we are going to use it later to refer to the active or selected  list-item. Next, we will add a role attribute with the value “option” to the list-item. Remember, we have added to the <ul> element a role value of “listbox” for screen readers to read it as a select-list (<select>). The listbox role requires that its direct children will have an “option” role so screen readers can parse and read it correctly.

Lastly, we will add to the list-item an “aria-selected” attribute of default value of “false”. This value will dynamically change as an outcome of user interaction.

Now you can add the list-item’s content. In the example below, I chose to add a thumbnail image to each list-item, but since it has only a decorative purpose, I hid it from screen readers using the aria-hidden=”true”. Note that, if you use an image with content that is essential for understanding the list-items, it shouldn’t be hidden for screen readers. It is also  to have an “alt” text that describes its content.

function comboListElement(itemData, i) {
    function createThumb() {
        if (itemData.thumb) {
            let thumb = document.createElement('img');
            thumb.src = itemData.thumb;
            thumb.className = 'result-item-thumb';
            thumb.setAttribute('aria-hidden', 'true');
            return thumb;
        }
    }

    function createText() {
        if (itemData.name) {
            let txt = document.createElement('span');
            txt.className = 'result-item-text';
            txt.innerText = itemData.name;
            return txt;
        }
    }

    let resultItem = document.createElement('li');
    resultItem.id = `result-item-${i}`;
    resultItem.className = 'result';
    resultItem.setAttribute('role', 'option');
    resultItem.setAttribute('aria-selected', 'false');

    if (createThumb()) resultItem.appendChild(createThumb());
    if (createText()) resultItem.appendChild(createText());

    return resultItem;
}

Note that the example above is not binding in terms of content structure. The only thing that matters is that each list-item will have a child text-node or another labeling attribute such as “aria-label” as its accessible name.

The DOM output from the example above should look something like this:

<li id="result-item-0"
    class="result"
    role="option"
    aria-selected="false">
    <img 
        src="path/to/img.jpg" 
        class="result-item-thumb"
        aria-hidden="true" />
    <span class="result-item-text">Some text</span>
</li>

Combo box states

The last topic we are going to cover in this post is the accessibility aspects of managing the combo box states. Let’s see which states the combo box is expected to have.

First, let’s look at the list-box – it can be expanded or collapsed. You will remember when we discussed the styling, we mentioned that, when the list is collapsed, it shouldn’t be available to assistive technologies. So its state is allegedly implicit, but we do have the <input> field that controls the list. This is the focused element while the user is interacting with the combo box. Therefore, we will add an “aria-expanded” attribute to the <input> element and initiate its value to “false“. This value should be updated as the list-box changes its state so that screen readers can correctly indicate it to the user. (Note that in our example we added the aria-expanded attribute to the combo box’s button as well.) 

Now let’s see how we are handling the state of the list items. It is probably a good time to discuss the terms “programmatic or visual focus” and “DOM focus”, and the difference between them. As mentioned before, the element that is actually focused while the user is interacting with the combo box, so we need a way to indicate which list-item is currently “focused” and can be selected when moving up and down the list-box using the arrow keys. The indication should be both visual for the benefit of keyboard-only users and in a way that can be read by assistive technologies. For the visual part, create a “selected” class for example, and set it with the same CSS rules that’s applied to the list-item “hover” state styling, add this class to the list item when it should be selected, and that should do the trick. To indicate the list-item state for assistive technologies, we will update and add some ARIA attributes. First, let’s deal with the “aria-selected” attribute we have added to the list-item element we’ve created before. When the list-item is supposed to be selected, the value of the list’s “aria-selected” attribute should be updated to “true” and “false” again when the selection moves to another list-item. We use the “aria-selected” attribute because it is part of the requirements of the WAI-ARIA’s listbox design pattern, but the actual selected list-item will be read by another ARIA attribute, and this is why we had to add a unique id to each list-item. You remember that the actual focus (DOM focus) is kept on the <input> element, so we want to have it referencing the selected list-item. So when a list-item is selected, we can add the “aria-activedescendant” attribute to the <input> element with the selected list-item’s id as its value. This will allow screen readers to read to the user the value of each of the list-items and complete the mental image of the widget and its state to the users.

That’s it, we have covered the main aspects that should be taken into account when building a combo box component from the accessibility point of view.

Summary

In the first part we have created the markup structure, defined some new semantics to elements by using the “role” attribute, defined relationships between the different combo box parts by using the “aria-owns” and “aria-controls” attribute and by some labeling methods. We have labeled the different elements and initiated some state attributes.

In the second part we have discussed the styling aspects that are essential for accessibility like focus indication and the importance of removing the list element from the accessibility tree when it is collapsed.

Lastly we discussed the accessibility aspects of the combo box’s state changes, what the required attributes are and how they should be managed.

Thank you for reading, I hope this post will help you to clearly understand what parts are affecting the accessibility of combo box in particular and other UI components that might require similar handling. I have tried to specifically address only aspects of accessibility in a way that can hopefully be applied in any way you choose to build your combo box. In any case, if you want to see the full context of the code, you can find it in the Github Gist and this live demo on CodePen.
This post and the example that accompanies it were inspired by the ARIA 1.1 Combobox with Listbox Popup Examples