Skip to content

[menu] Remove invalid aria-owns owner span from menu portals#5002

Closed
lifeiscontent wants to merge 1 commit into
mui:masterfrom
lifeiscontent:fix/menu-aria-required-children
Closed

[menu] Remove invalid aria-owns owner span from menu portals#5002
lifeiscontent wants to merge 1 commit into
mui:masterfrom
lifeiscontent:fix/menu-aria-required-children

Conversation

@lifeiscontent

Copy link
Copy Markdown

Fixes #4004

Menu.Portal renders a visually hidden <span aria-owns> inside the
menu/menubar. axe flags it under aria-required-children because a bare
span is not an allowed child of the menu/menubar role
(WAI-ARIA spec).

Root cause

The span comes from FloatingPortal, which renders <span aria-owns={portalId}>
to keep VoiceOver's virtual cursor in document order for generic portaled
panels. The span and the focus guards share one gate (shouldRenderGuards),
so the span can't be dropped without also losing tab-order management.

For a menu the span is redundant: the trigger already exposes the popup
through aria-haspopup, aria-controls, and aria-expanded.

Changes

  • Add a renderOwner flag to FloatingPortal (mirroring the existing
    renderGuards flag) that suppresses only the owner span, leaving the
    focus guards and tab-order handling in place.
  • Pass renderOwner={false} from Menu.Portal. Submenus and Menubar
    go through the same portal, so they're covered too. Other portals are
    unchanged and still render the span.
  • Add tests: the menu portal no longer renders span[aria-owns], and the
    generic portal still does while keeping its focus guards.

Notes

This removes the node outright rather than hiding it with aria-hidden
(as in #4027). Adding aria-hidden to the owner is contested upstream
(floating-ui/floating-ui#3403) because it can defeat the virtual-cursor
ordering the span exists for. Since the span is redundant in menus,
dropping it avoids that trade-off.

@lifeiscontent lifeiscontent requested a review from atomiks as a code owner June 10, 2026 08:39
Copilot AI review requested due to automatic review settings June 10, 2026 08:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Updates Menu + FloatingPortal behavior to avoid rendering an aria-owns “owner” span that can violate ARIA child-role requirements, while keeping focus-guard behavior intact.

Changes:

  • Disable the owner span[aria-owns] for Menu.Portal by passing renderOwner={false} to FloatingPortal.
  • Add a new renderOwner?: boolean prop to FloatingPortal (default true) to control rendering of the owner node.
  • Add/extend tests for both Menu.Portal and FloatingPortal to validate the new behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
packages/react/src/menu/portal/MenuPortal.tsx Stops rendering the owner node for menus by opting out via renderOwner={false}.
packages/react/src/menu/portal/MenuPortal.test.tsx Adds a regression test asserting the owner aria-owns span is not present for menus.
packages/react/src/floating-ui-react/components/FloatingPortal.tsx Introduces renderOwner prop and conditions owner node rendering on it.
packages/react/src/floating-ui-react/components/FloatingPortal.test.tsx Adds tests to verify default owner rendering and opt-out behavior while keeping focus guards.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/react/src/floating-ui-react/components/FloatingPortal.tsx
Comment thread packages/react/src/floating-ui-react/components/FloatingPortal.tsx
@pkg-pr-new

pkg-pr-new Bot commented Jun 10, 2026

Copy link
Copy Markdown

commit: a6e4e94

@code-infra-dashboard

code-infra-dashboard Bot commented Jun 10, 2026

Copy link
Copy Markdown

Bundle size

Bundle Parsed size Gzip size
@base-ui/react 🔺+35B(+0.01%) 🔺+20B(+0.01%)

Details of bundle changes

Performance

Total duration: 1,270.90 ms -155.21 ms(-10.9%) | Renders: 50 (+0) | Paint: 1,921.26 ms -227.60 ms(-10.6%)

Test Duration Renders
Scroll Area mount (300 instances) 80.61 ms ▼-33.24 ms(-29.2%) 3 (+0)
Tooltip mount (300 contained roots) 43.46 ms ▼-19.44 ms(-30.9%) 1 (+0)
Dialog mount (300 instances) 54.39 ms ▼-16.01 ms(-22.7%) 1 (+0)

9 tests within noise — details


Check out the code infra dashboard for more information about this PR.

`Menu.Portal` rendered a visually hidden `<span aria-owns>` inside the
menu/menubar, which axe flags under `aria-required-children`: a bare
`span` is not an allowed child of the `menu`/`menubar` role.

The span comes from `FloatingPortal`, where it keeps VoiceOver's virtual
cursor in order for generic portaled panels. In a menu it is also
redundant, since the trigger already exposes the popup through
`aria-haspopup`, `aria-controls`, and `aria-expanded`.

Add a `renderOwner` flag to `FloatingPortal` that drops only the owner
span while leaving the focus guards and tab-order handling intact, and
pass it from `Menu.Portal`. Other portals are unchanged.
@lifeiscontent lifeiscontent force-pushed the fix/menu-aria-required-children branch from bf0d6d4 to a6e4e94 Compare June 10, 2026 08:42
@netlify

netlify Bot commented Jun 10, 2026

Copy link
Copy Markdown

Deploy Preview for base-ui ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit bf0d6d4
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6a2922af5ba13f0009989c96
😎 Deploy Preview https://deploy-preview-5002--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Jun 10, 2026

Copy link
Copy Markdown

Deploy Preview for base-ui ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit a6e4e94
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6a2923787c9ea90008bab813
😎 Deploy Preview https://deploy-preview-5002--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@lifeiscontent

Copy link
Copy Markdown
Author

Closing this. Per the discussion in floating-ui/floating-ui#3424, the owner span isn't redundant after all: it places the portaled submenu correctly in the accessibility tree, so removing it for menus (what this PR does) would regress that placement rather than fix the underlying problem. The real gap is in axe-core's aria-required-children rule, which reads the DOM tree and not the accessibility tree when aria-owns reparents an element. That's tracked at dequelabs/axe-core#4048 (and the precedence question at #926). Leaving the markup as-is, thanks all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[menubar] when dropped down, has children which are not allowed: span[aria-owns]

3 participants