Skip to main content

Search rules

Search all UI Guides rules by name, category, or source

interactionsARIA

Keep Focus in the Input With aria-activedescendant

SHOULD

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.

Built by Gleb Stroganov, design engineer at Evil Martians.

The rules come from other people's skills and guidelines — Vercel, Rauno Freiberg, @Ibelick, impeccable, Emil Kowalski, Tailwind, RAMS — each one credited on the Sources page. The work here is extraction and wiring: every rule is pulled into one corpus, given a good and a bad example you can operate, a MUST/SHOULD/NEVER rule an agent can paste, and a link back to where it came from.