Repository navigation
bug: ion-button post-load aria-label stays on host and native control (duplicate VoiceOver stop) #31659
Description
Prerequisites
- I have read the Contributing Guidelines.
- I agree to follow the Code of Conduct.
- I have searched for existing issues that already report this problem, without success.
Ionic Framework Version
- v9.x
Current Behavior
Setting aria-label on an ion-button host works on the initial inherit: Ionic copies it onto the shadow native <button> / <a> and removes it from the host.
Any post-load write of aria-label on the host (Angular [attr.aria-label], including translated labels that update on change detection / language change) is copied onto the native control by createAriaAttributeController, but left on the host as well.
VoiceOver on iOS then exposes the same control twice (labelled host group + labelled native button). This also makes automation that queries by accessible name (~More capture options) match two elements.
This matches the documented limitation in createAttributeController:
Only the initial copy removes the attributes from the host, so a value written after load sits on the host as well as on the element it is applied to…
ion-button already treats role as host-owned for this dual-presence reason. aria-label still has the hole.
Related closed work (#30626 / #31264) fixed reactivity of host ARIA onto the native control, but did not clear post-load host attributes after reflecting them.
Expected Behavior
After Ionic reflects a host aria-label onto the native control, the host should not keep a labelled copy that appears as a second stop in the accessibility tree — for post-load writes as well as the initial inherit (same treatment the controller already documents as necessary for role).
Using [attr.aria-label] / dynamic host labels with Angular should result in a single accessible name on the focusable control.
Steps to Reproduce
- Open the reproduction: https://gist.githubusercontent.com/masimplo/49c9ccb3e4ba928a64d89090ab3c5de7/raw/ion-button-aria-label-duplicate.html (or https://gist.github.com/masimplo/49c9ccb3e4ba928a64d89090ab3c5de7)
- Observe the dump after load:
hostAriaLabelisnull,nativeAriaLabelis"Scan"(initial inherit cleared the host). - Click Re-apply host aria-label (simulates Angular
[attr.aria-label]rewriting the host after load). - Observe:
bothLabelled: true— host and native both havearia-label="Scan". - On an iOS WebView / Capacitor app with VoiceOver, swipe through an icon-only
ion-buttonlabelled this way after navigation or a language change: the label is announced twice / two accessibility nodes appear.
Code Reproduction URL
https://gist.github.com/masimplo/49c9ccb3e4ba928a64d89090ab3c5de7
Ionic Info
Ionic:
Ionic Framework : @ionic/angular 9.0.7 / @ionic/core 9.0.7
Utility:
(repro is a CDN @ionic/core@9.0.7 single HTML page; also reproduced in a Capacitor iOS WebView)
System:
NodeJS : 22.x
Additional Information
- Angular continuously re-applies
[attr.aria-label]on change detection, so the “post-load write” path is the normal path for Ionic Angular apps — not an edge case. - Suggested fix direction: when the attribute controller observes a watched ARIA attribute written on the host, copy it into
attributesand remove it from the host again (asinheritAttributesdoes on the initial copy), at least foraria-label/ naming attributes that make the host appear in the a11y tree.ion-buttonalready excludesrolefrom watching for the same dual-node reason.
Activity
masimplo commentedon Oct 27, 2026
Working on a PR to clear post-load ARIA writes from the host after they are reflected onto the native control (same as the initial inherit), so VoiceOver no longer sees a duplicate labelled node.
thetaPC commentedon Oct 27, 2026
Thanks for the report!
Verified on 9.0.9. When
aria-labelis written toion-buttonafter it has loaded, which is what Angular's[attr.aria-label]does whenever the bound value changes, Ionic copies the value onto the inner native button but also leaves it on theion-buttonelement itself. That element has no role, so browsers expose it as a separate labelled node beside the button, which is the second stop you are seeing.ion-itemandion-cardbehave the same way.Workaround, verified on 9.0.9: for an icon-only button, bind the label to the icon instead of the button. The button takes its name from the icon, there is only one node, and changing or clearing the label keeps working:
Tracked as a bug in
core, and the fix review continues on #31660.