Keep Focus in the Input With aria-activedescendant
SHOULD: Default to roving `tabindex` (real DOM focus buys you scroll-into-view, the focus ring and focus events); reach for `aria-activedescendant` only where DOM focus must STAY in a text input — combobox, editable grid cell, tag input — because the moment focus leaves the `<input>` keystrokes stop landing, the caret vanishes, an IME composition dies mid-word and the mobile keyboard drops. Its costs are yours: stable `id` per option, style the active option yourself (it has no `:focus`), and `scrollIntoView({ block: "nearest" })` by hand.
Point at the active option instead of moving focus whenever the user must keep typing
<input role="combobox" aria-controls="lb" aria-activedescendant={activeId} />
<ul id="lb" role="listbox"><li id="opt-2" role="option" aria-selected /></ul>Bad
Good
Why it matters
These are the two focus-management techniques for a composite, and they are not interchangeable. Roving tabindex moves REAL DOM focus onto the active child, which is why it is the default: the browser scrolls that child into view, paints the focus ring, and fires focus events, all for free. But it has one fatal domain — anywhere the user must keep typing. In a combobox, an editable grid cell, or a tag input, ArrowDown must move the highlight WITHOUT moving focus, because the moment focus leaves the <input> the keystrokes stop landing, the caret disappears, an IME composition is destroyed mid-word, and on mobile the software keyboard drops away.
That is what aria-activedescendant is for: DOM focus stays on the container (the input), and the attribute holds the id of the active descendant, so assistive technology announces the option while the text field keeps receiving keys. The costs are real and yours to pay: every option needs a stable id, the container needs aria-activedescendant updated on every move, you must style the active option yourself because it has no :focus, and you must scroll it into view yourself with scrollIntoView({ block: "nearest" }) — the free ride roving tabindex got from the browser is exactly what you are giving up.