Max Design

Published:

This article explains aria-activedescendant in plain language, starting with a familiar native <select> and then showing what changes when developers build their own custom version.

1. Native control

Let’s start with a normal HTML <select>, as this will help us understand what aria-activedescendant is trying to achieve.

<label for="animal">Choose an animal</label>

<select id="animal">
  <option>Dog</option>
  <option>Cat</option>
  <option>Rabbit</option>
</select>

A native control like this comes with a lot of behaviour already built in.

The browser knows its name:

"Choose an animal"

It knows its role:

"This is a select control with a list of options."

It also knows the normal keyboard behaviour for that control. A keyboard user can move to it with Tab, then use the arrow keys to move through the options.

For example, pressing ↓ moves from “Dog” to “Cat”:

Dog     ← was current
Cat     ← current option
Rabbit

This behaviour is already understood by the browser. The developer does not have to invent which keys should work.

It is also familiar to users. If someone has used a select before, they can reasonably expect the arrow keys to move through its options.

Most importantly, as the current option changes, the browser also passes that information to assistive technology.

As the user moves from “Dog” to “Cat” to “Rabbit”, the screen reader can announce those options.

A lot is happening automatically to help screen readers understand the control:

That is one of the big advantages of using a native HTML control.

2. Non-native control

So why would a developer choose not to use it?

Sometimes a native select just cannot do what's needed. For example:

In these situations, a developer may choose to build their own version of a <select> using generic HTML elements, then add the missing behaviour back in with JavaScript.

The markup may look like this:

<div>Choose an animal</div>

<div>Dog</div>
<div>Cat</div>
<div>Rabbit</div>

Visually, it might look almost exactly the same.

The text "Choose an animal" is there, so people looking at the page can understand the name.

But the other features we got from the native control are missing.

The browser does not automatically know:

The developer now has to recreate those things.

ARIA can help explain to screen readers what the control and its parts are, such as its name and role. The control also needs a tabindex="0" so that it can receive keyboard focus, since a plain <div> is not focusable on its own:

<div
  role="listbox"
  aria-label="Choose an animal"
  tabindex="0"
>

ARIA can also define the role of each option:

<div role="option">Dog</div>
<div role="option">Cat</div>
<div role="option">Rabbit</div>

ARIA does not provide the keyboard behaviour, so the developer still needs JavaScript to add it.

For example, JavaScript can listen for the down-arrow key and keep track of which option the user has moved to:

Dog     ← was current
Cat     ← current option
Rabbit

However, in some custom controls, the developer does not actually move keyboard focus onto “Dog”, then “Cat”, then “Rabbit”.

Instead, keyboard focus stays on the main control while JavaScript keeps track of which option the user is moving through.

Keeping keyboard focus on the main control can be useful in some custom controls. For example, in an editable combobox, focus can stay in the text field while the user moves through a list of suggestions.

So the browser needs to be told:

"The keyboard is still on this control, but Cat is the option the user is currently on."

That is where aria-activedescendant comes in.

The name itself is a clue. It is made up of two words, active and descendant.

A descendant just means an element that lives inside another one. Dog, Cat and Rabbit are all descendants of the main control, the same way children live inside a family tree.

So aria-activedescendant identifies which descendant of the main control is currently active.

Each option is given an id, and the main control’s aria-activedescendant value is set to match the id of the currently active option. An aria-selected="true" attribute is also added to that option to mark it as the current selection:

<div
  role="listbox"
  aria-label="Choose an animal"
  tabindex="0"
  aria-activedescendant="dog"
>
  <div role="option" id="dog" aria-selected="true">Dog</div>
  <div role="option" id="cat">Cat</div>
  <div role="option" id="rabbit">Rabbit</div>
</div>

Which basically means:

"Dog is the active item inside me."

When the user presses ↓, JavaScript changes the main control’s aria-activedescendant to "cat", and moves aria-selected="true" onto the Cat option.

aria-activedescendant does not move keyboard focus. Keyboard focus stays on the main control.

Instead, JavaScript updates the value of aria-activedescendant as the user moves through the options. The changing value tells the browser and screen reader which option should be treated as the current one.

The important point is that focus itself is not moving between “Dog”, “Cat” and “Rabbit”. JavaScript is changing aria-activedescendant to tell assistive technology which option should be treated as if focus were there.

The active option may also be visually highlighted as the user moves through the list, but keyboard focus remains on the main control.

Takeaway

Native control: the browser already knows the name, role, expected keys, how movement works, and which option to report.

Custom control: in some cases the developer has to recreate those behaviours. Where needed, JavaScript can be used to handle the movement, and aria-activedescendant can be used to tell assistive technology which item is currently active.

Test page

Testing aria-activedescendant