[combobox] VoiceOver + Safari reads no options after ArrowDown opens the list
Bug report
Current behavior
With VoiceOver + Safari on macOS, pressing ArrowDown in an empty, closed Combobox.Input opens the list, and VoiceOver announces expanded list 10 items. After that, no option is ever read. Further ArrowDown presses are silent, and Enter commits nothing audible.
The same combobox reads every option when the list is opened another way:
- typing a character first, then arrowing:
Apple (1 of 6),Banana (2 of 6), … - opening with
Combobox.Trigger, then arrowing:Apple (1 of 10),Banana (2 of 10), …
In Chromium's accessibility tree everything looks correct. Focus stays on the input, and aria-activedescendant moves to each option. axe passes. Only the ArrowDown-to-open path is silent.
Expected behavior
ArrowDown on a closed combobox opens the list and VoiceOver reads the highlighted option, then each option as the highlight moves, as it does when the list is opened by typing or by the trigger.
Reproducible example
Any single-select Combobox whose input sits outside the popup (the default docs anatomy) with ~10 items:
import { Combobox } from '@base-ui/react/combobox';
const fruits = ['Apple', 'Banana', 'Blueberry', 'Cherry', 'Grape', 'Lemon', 'Mango', 'Orange', 'Peach', 'Pear'];
export default function Repro() {
return (
<Combobox.Root items={fruits}>
<label htmlFor="fruit">Fruit</label>
<Combobox.Input id="fruit" placeholder="Pick a fruit…" />
<Combobox.Trigger aria-label="Show fruits" />
<Combobox.Portal>
<Combobox.Positioner>
<Combobox.Popup>
<Combobox.List>
{(item: string) => (
<Combobox.Item key={item} value={item}>
{item}
</Combobox.Item>
)}
</Combobox.List>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>
);
}Steps: turn on VoiceOver, focus the input (Tab or click), press ArrowDown, then ArrowDown again.
Isolated with plain HTML (no React, no Base UI)
A hand-written ARIA combobox with the same key handling reproduces the silence. The trigger is the list's wrapper becoming visible in the same moment aria-activedescendant is set:
| Plain-HTML variant (ArrowDown opens and highlights option 1) | VoiceOver |
|---|---|
<ul role="listbox"> toggles hidden itself |
reads every option |
listbox portalled to <body>, toggles hidden itself |
reads every option |
listbox inside a wrapper <div>; the wrapper toggles hidden (no role, no tabindex) |
silent |
| same wrapper, first highlight set 150ms after the wrapper is shown | reads every option |
| same wrapper, opened with no highlight; the next ArrowDown highlights | reads every option |
working variant plus aria-hidden="true" on <main> while open |
reads every option (so hiding outside nodes is not the cause) |
Core of the silent variant:
<input id="cb" role="combobox" aria-expanded="false" aria-controls="list" aria-autocomplete="list">
<!-- appended to <body> -->
<div id="popup" hidden><ul id="list" role="listbox" aria-label="Fruit">…10 role="option" items…</ul></div>
<script>
input.addEventListener('keydown', (e) => {
if (e.key !== 'ArrowDown') return;
e.preventDefault();
if (popup.hidden) {
popup.hidden = false;
input.setAttribute('aria-expanded', 'true');
setActive(0); // same task as un-hiding the wrapper → silent
// setTimeout(() => setActive(0), 150); // → every option is read
} else {
setActive(active + 1);
}
});
</script>Base UI's ArrowDown-to-open path has this shape. The popup's wrappers appear, and useListNavigation's initial sync (focusItemOnOpen: 'auto' with the key recorded) sets the highlight in the same commit.
A deferred highlight in Base UI is not enough on its own
I tried patching floating-ui-react/hooks/useListNavigation so that for virtual lists the "Initial sync" highlight is set 150ms after open. Unit tests and Chromium behaved correctly, but real VoiceOver + Safari with real keys was nondeterministic:
| Build | Trials that read options | Failure mode |
|---|---|---|
| @base-ui/react 1.8.0 | 0 of 5+ | silent, DOM focus stays on the input |
| 1.8.0 + deferred first highlight | 3 of 10 | VoiceOver says "scroll area …" on open and DOM focus moves to body |
Ruled out (4 trials each): page scroll (scrollY stays 0), markOthers aria-hidden on outside nodes (forced ariaHidden: false → 1 of 4), and an unrelated role="alert" on the page (removed → 1 of 4). Keydown events dispatched from page script never reproduce the problem. It needs a real key press with VoiceOver running.
Trials were captured by polling VoiceOver's AppleScript content of last phrase while System Events sent the keys, with document.activeElement and the active option mirrored into document.title. Happy to share the harness or raw transcripts.
Base UI version
v1.8.0 (latest on npm at the time of writing)
Which browser are you using?
Safari 26.6.2
Which OS are you using?
macOS 26.6.2 (25G83)
Which assistive tech are you using (if applicable)?
VoiceOver (macOS 26.6.2), default settings
Additional context
Chromium shows the correct accessibility tree for every variant; only VoiceOver + WebKit loses track. Part of this may be a WebKit bug. Still, Base UI setting the initial highlight in the same commit that exposes the popup is the part a library can change. In our testing, the plain-HTML equivalent of "expose the popup, then highlight" is reliably read.
Source: mui/base-ui