[combobox] Non-modal popup aria-hides outside content but leaves it tabbable (axe: aria-hidden-focus)
- Dominant language
- TypeScript
- Stars
- 10.9k
- Forks
- 543
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 101
Description
# Bug report
## Current behavior
When a combobox popup opens, `FloatingFocusManager` hides everything outside the floating tree from assistive technology:
```ts
const ariaHiddenCleanup = markOthers(insideElements, {
ariaHidden: modal || isUntrappedTypeableCombobox,
mark: false,
});
```
The `isUntrappedTypeableCombobox` half makes this unconditional for comboboxes — the outside content gets `aria-hidden="true"` even with the default `modal={false}`, whose JSDoc promises "user interaction with the rest of the document is allowed".
The hidden elements are not removed from sequential focus navigation, so every focusable element outside the popup (nav links, buttons, inputs) becomes an axe `aria-hidden-focus` violation (serious, WCAG 4.1.2) the moment the popup opens. Beyond the audit noise it is a real interop gap: Tab can move DOM focus into content a screen reader user cannot perceive.
## Expected behavior
Content hidden from the accessibility tree while the popup is open should also be removed from sequential focus navigation.
Note that the fix proposed in #4678 for the modal case — passing `inert: true` — is **not** available here: `inert` also blocks pointer interaction, which the non-modal contract explicitly keeps allowed. The two cases need either different mechanisms or a shared one that does not rely on `inert`.
## Suggested fix
`markOthers` is a port of [`aria-hidden`](https://github.com/theKashey/aria-hidden), which already solves this with its `inertOthers` mode: alongside `aria-hidden="true"`, focusable descendants of the hidden subtrees get `tabindex="-1"`, restored on cleanup. Porting that behavior (integrated with the existing per-node counters so nested opens stay balanced) fixes the combobox case and, as a side effect, #4678 as well — without changing pointer behavior for non-modal surfaces.
A tested reference implementation lives here as a `pnpm patch` against `1.7.0`: [patches/@base-ui__react@1.7.0.patch](https://github.com/michaldudak/based-bugz/blob/master/patches/%40base-ui__react%401.7.0.patch). With it applied, an axe scan (`wcag2a`/`wcag21aa`) of a page with an open combobox popup reports no violations, popup close restores the original tab order (including pre-existing `tabindex` values and roving-tabindex widgets), and modal dialog scans stay clean.
## Reproducible example
Any `Combobox` on a page that has focusable content outside it:
1. Open the popup.
2. Run axe-core (e.g. `@axe-core/playwright` with `wcag2a` tags) on any region containing focusable elements outside the popup.
3. `aria-hidden-focus` (impact: serious) is reported for each such region.
Found by the axe suite of a Combobox evaluation app: [tests/a11y.spec.ts](https://github.com/michaldudak/based-bugz/blob/master/tests/a11y.spec.ts).
## Base UI version
1.7.0
## Which browser are you using?
Chrome (macOS and Linux, also reproduced in CI)
## Which assistive tech are you using (if applicable)?
axe-core via `@axe-core/playwright`; the Tab-into-hidden-content behavior is observable with keyboard alone.
## Additional context
Related: #4678 tracks the same root mechanism for modal dialogs, where `inert` is a viable fix. This issue covers the non-modal combobox path where it is not.
Contributor guide
Research direction
Start with FloatingFocusManager and its markOthers call, then review the linked tests/a11y.spec.ts reproduction and the existing cleanup behavior. Verify the change with an open non-modal Combobox: axe reports no aria-hidden-focus violations, Tab cannot enter hidden content, and closing restores original and roving tabindex values without blocking pointer interaction.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- accessibility, frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100