Accessibility by Default: Comboboxes
Hi, and welcome back to Accessibility by Default. If you’re new here, welcome! If you’re a returning reader, thank you so much for coming back.
This week, we’re going to look into comboboxes. I feel that, on the surface, these components can appear very simple, but when you actually start to examine the functionality and accessibility behind them, it can become quite complicated.
What is it?
So, what do we mean by a combobox?
A combo box is a bit like if an input box and a select box had a baby, and then that baby was babysat by an accordion because it takes bits and pieces of all three of those components and puts them into one.
It has the look of an input, the selection behaviour of a select, and some of the disclosure and state concepts you see in an accordion.
At its simplest, a combobox is an input associated with a popup containing possible values. That popup might contain suggestions that change as the user types, or it might contain a predefined set of values that the user can choose from.
When to use it?
The main use of these components is to improve the user experience by allowing users to type into the input while the space below populates with relevant, selectable options.
A good use case for this component is when a user needs to select their country from a large list. Rather than making them scroll through a huge list of countries, they can start typing, and the combobox can filter the available options to show relevant suggestions. This is an example of a combobox with list autocomplete, where the suggestions change based on what the user types.
Comboboxes can also be useful when users are allowed to enter their own value, but suggestions are provided to help them get there more quickly. Search fields are a good example of this.
Comboboxes should be used to solve a real problem. If the user simply needs to select one value from a small, predictable list, a native <select> may be a much better option.
We should also understand the difference between a combobox and a filterable dropdown. Both components can look similar, but each has its own purpose. A combobox allows the user to enter a value while providing suggestions or options through a popup. A filterable list is primarily used for narrowing down an existing set of options, rather than allowing the user to enter an arbitrary value. Before trying to build and implement a combobox, think about what the user is trying to do.
Accessibility Considerations
When we look at the accessibility behind comboboxes, there are some similarities to accordions and disclosure that we’ve spoken about in my tabs, accordions and disclosures article, mostly around opening and closing the popup. But a combobox does have its own interaction and accessibility requirements.
Aria roles
Initially, your input should have role="combobox" and aria-expanded="false". When the popup containing the suggestions is opened, aria-expanded should change to true.
As with all input boxes for proper accessibility, it should have an associated label with it (check out my form controls article here). And then we have aria-autocomplete, which should match the autocomplete behaviour of your combobox.
There are four possible values for the aria-autocomplete:
- None — the popup contains options, but what appears doesn’t change based on what the user types
- List — the popup presents a list of suggestions that correspond to what the user has typed
- Inline — the component provides an inline completion for the value the user is entering
- Both — You get filtered suggestions and the inline completion.
These values describe different types of combobox functionality, so you need to make sure that the value matches the behaviour your component actually provides.
aria-activedescendant identifies which option is currently active while focus remains on the combobox input. This allows users to move through the suggestions with the arrow keys without actually moving the DOM focus into the listbox.
The popup itself should have an appropriate role. For the example we’re looking at, that’s role="listbox", with each suggestion represented by an option.
The currently active option and the selected option aren’t necessarily the same thing. aria-activedescendant identifies the option currently being navigated to, while aria-selected communicates selection state where the pattern requires it.
Here’s a simplified example:
<label for="country">Country</label>
<div class="combobox">
<input
id="country"
type="text"
role="combobox"
aria-expanded="false"
aria-controls="country-options"
aria-autocomplete="list"
/>
<ul id="country-options" role="listbox" hidden>
<li
id="country-option-1"
role="option"
aria-selected="false"
>
Canada
</li>
<li
id="country-option-2"
role="option"
aria-selected="false"
>
Cambodia
</li>
<li
id="country-option-3"
role="option"
aria-selected="false"
>
Cameroon
</li>
</ul>
</div>In your JavaScript, you need a state mechanism that will keep the attributes in sync. When the user focuses on the input and starts to type, the script should toggle the aria-expanded to true, remove the hidden attribute from the listbox, and then change the aria-activedescendant to match the option index the user uses the arrow keys to navigate to. Then, when they blur or press the escape key, it will revert them so that aria-expanded is false, listbox receives hidden again, and aria-activedescendant is cleared.
Remember that aria-controls should reference the popup that the combobox controls. It is valid for the referenced element to be hidden when the combobox is collapsed. A listbox is the default popup type for a combobox, so aria-haspopup="listbox" is implied and doesn't need to be added in this example. If you were using a different popup type, such as a grid, tree, or dialog, then aria-haspopup should identify that type.
Keyboard interactions
This is where a combobox can become more complicated than a normal input. When the user is focused on the combobox, the down arrow can open the popup and move the active option to the first option in the dropdown, and the up arrow can be used to navigate to the last option in the dropdown. Once the user is navigating the list of options the up arrow and down arrow should move between options, and enter will accept the currently active option. If the user presses Escape, then the popup should be closed (if it is open). When an autocomplete suggestion is active, Enter will accept that suggestion and update the value of the combobox.
The main thing to remember here is that behaviour can vary depending on the type of combobox you’re implementing, but keyboard interaction should be predictable and consistent.
Focus management
Focus management, in my opinion, is one of the hardest things when it comes to comboboxes. These are the things that you must remember
- Focus is initially owned by the input
- Arrow keys are used to change the active option
aria-activedescendantis used to communicate the active option while the DOM focus remains on the input- Focus doesn’t necessarily move into the listbox
- Closing the popup should return you to a predictable state, such as returning the user to the input box
One of the important things to understand here is the difference between DOM focus, visual focus, and selection. A user can be moving through the suggestions while the browser’s actual focus stays on the input. By using aria-activedescendant, this allows assistive technologies to understand which option is active.
Get Josh Allan’s stories in your inbox
Join Medium for free to get updates from this writer.
This is the part where the component can look visually correct, but it can become confusing or unusable for assistive technologies if the state isn’t correct.
What happens when an option is selected?
Another thing that we need to think about is what happens when the user selects one of the options. In the simplest example, the value of the selected option becomes the value of the input, and the list of selectable options closes so that the user can continue interacting with the rest of the page.
But this isn’t always quite as straightforward because we also need to consider whether the user needs to select one of the suggested options or whether they can enter their own value. If we go back to the country example, we don’t want the user to enter something that isn’t a country, so the suggestions are there to help them find the correct option.
On the other hand, if we’re using the combobox for something like a search field, the suggestions might just be there to help the user find what they’re looking for. They should still be able to enter their own search term, even if it isn’t one of the suggestions.
This is a distinction to make when designing the component because a combobox isn’t necessarily just a fancy select box. The behaviour of the component needs to match what we’re actually asking the user to do.
And if all we need is for the user to select one value from a predefined list, we should also ask ourselves whether we need a combobox at all.
A native <select> already gives us a well-understood and accessible interaction without having to recreate all of that behaviour ourselves.
Sometimes, the most accessible solution is simply not building the complicated thing in the first place.
Announcements and dynamic updates
When a user types into the input, the suggestions can change, the popup can open or close, the active option can change, and the selected value can be updated. For someone using a screen reader, we need to make sure they have enough information to understand what is happening.
This doesn’t mean that we should announce every single change. In fact, doing that could make the experience incredibly noisy and frustrating for someone using a screen reader.
Instead, we need to make sure that the state changes are exposed correctly through the combobox pattern.
For example, if a user types Can and a list of suggestions appears, they should be able to understand that suggestions are available and then use their arrow keys to move through them. As they move through the list, the active option should be communicated to them, and when they make a selection, the updated value should be available to assistive technology.
We also need to think about situations where the suggestions are loaded dynamically. If the user types something and the component needs to make a request before displaying the results, we shouldn’t leave the user wondering whether anything is happening.
Loading, no results, and updated suggestions are all states that need to be considered as part of the component.
The thing to remember is that accessibility isn’t just about making the options available — it’s about making the state of the component understandable as it changes.
Common mistakes
I think that this is one of those components that can be very JavaScript heavy, simply because of the interactions and the amount of state that needs to be managed.
You need to update different states depending on what the user is doing with the combobox. If they’re actively typing, you need to populate the suggestions and update the popup state. If they’re navigating through the options, you need to keep the active option and aria-activedescendant in sync. You also need to make sure the keyboard interactions work as expected while keeping focus predictable.
Another common mistake is that screen readers aren’t being given enough information about what has changed.
If suggestions appear, disappear, or change without the component exposing the appropriate state, users can be left wondering what is happening. Focus can also become problematic if it unexpectedly moves to the wrong element or disappears altogether.
And the biggest one is creating and using this whole element when you don’t need to.
If you have a selection of 20 different known and predictable values, you probably don’t need a whole combobox implementation. You could probably get away with using a native <select> with those options.
However, if you’ve got a list of over 100 countries to choose from, I’d help the user out by allowing them to narrow down their selection by using a combobox.
Key takeaways
My key takeaway from this component is that you need to keep track of all of the different states and interactions to make sure that the user doesn’t get lost while using the component, regardless of whether they’re using a keyboard or a mouse.
I also think that one of the big things to remember is that a combobox might look like a single component, but there are several moving parts underneath. You have the input, the popup, the options, the filtering, the focus management, and the interactions.
Think of it like a jigsaw.
Each piece might be straightforward, but the accessibility problems appear when you try to make all of those pieces work together.
And before you build one, ask yourself whether you actually need one. If a native control already gives you the behaviour you need, use it.
References
WAI-ARIA Authoring Practices Guide — Combobox Pattern
Accessibility by Default is a series where I explore practical accessibility techniques from the perspective of a frontend engineer. Rather than repeating specifications, the goal is to explain the engineering decisions and patterns that make accessible interfaces easier to build from the start.
You can find the full series on my profile










