Skip to content

bug: ion-button post-load aria-label stays on host and native control (duplicate VoiceOver stop) #31659

Open

Description

@masimplo

Prerequisites

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

  1. Open the reproduction: https://gist.githubusercontent.com/masimplo/49c9ccb3e4ba928a64d89090ab3c5de7/raw/ion-button-aria-label-duplicate.html (or https://gist.github.com/masimplo/49c9ccb3e4ba928a64d89090ab3c5de7)
  2. Observe the dump after load: hostAriaLabel is null, nativeAriaLabel is "Scan" (initial inherit cleared the host).
  3. Click Re-apply host aria-label (simulates Angular [attr.aria-label] rewriting the host after load).
  4. Observe: bothLabelled: true — host and native both have aria-label="Scan".
  5. On an iOS WebView / Capacitor app with VoiceOver, swipe through an icon-only ion-button labelled 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 attributes and remove it from the host again (as inheritAttributes does on the initial copy), at least for aria-label / naming attributes that make the host appear in the a11y tree. ion-button already excludes role from watching for the same dual-node reason.

Activity

  1. masimplo commented on Oct 27, 2026

    @masimplo
    ContributorAuthor

    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.

  2. thetaPC commented on Oct 27, 2026

    @thetaPC
    Contributor

    Thanks for the report!

    Verified on 9.0.9. When aria-label is written to ion-button after 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 the ion-button element 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-item and ion-card behave 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:

    <ion-button>
      <ion-icon slot="icon-only" name="scan-outline" [attr.aria-label]="label"></ion-icon>
    </ion-button>

    Tracked as a bug in core, and the fix review continues on #31660.

  3. added
    type: buga confirmed bug report
    a11yAccessibility related issues
    on Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    a11yAccessibility related issuespackage: core@ionic/core packagetype: buga confirmed bug report

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      ⌂ Proxy