Skip to content

Set AccessibilityNodeInfo stateDescription for checked state on Android - #59009

Open
BilalWalker wants to merge 2 commits into
react:mainfrom
BilalWalker:android-a11y-state-description
Open

BilalWalker wants to merge 2 commits into
react:mainfrom
BilalWalker:android-a11y-state-description

Conversation

@BilalWalker

@BilalWalker BilalWalker commented Oct 12, 2026 •

Copy link
Copy Markdown

Summary:

On Android, TalkBack announces no state for an unchecked React Native checkbox or radio: role="checkbox" / role="radio" with aria-checked={false} (or accessibilityState={{checked: false}}) is read as just "Email me offers, tickbox", so a TalkBack user cannot tell an unchecked option from an element with no state at all (#58739). A role="switch" with checked: true is read as "checked. on." (#58588).

Both have the same root cause. Since API 30 TalkBack speaks the state of a checkable node from AccessibilityNodeInfo.stateDescription, and only suppresses its own generic "checked" when one is set. Native CompoundButton (CheckBox, RadioButton, Switch) always sets it; ReactAccessibilityDelegate.setState only set isCheckable / isChecked and never called setStateDescription. No JavaScript prop reaches stateDescription, so apps cannot work around this.

This change sets stateDescription in the same place isCheckable / isChecked are set:

  • role switch: on / off, from the existing state_on_description / state_off_description resources, which are already translated in all 65 locales under uimanager/res.
  • any other checkable (checkbox, radio, togglebutton, or no role) and unchecked: not checked, from a new state_unchecked_description resource.
  • any other checkable and checked: left unset, so TalkBack keeps announcing it in the device language ("Ticked", "Aktiviert", ...), exactly as today.

Only the case TalkBack is currently silent on gets a new string. Translations of the uimanager strings come from Meta's internal translation pipeline ("Intl Scheduler" commits), not from pull requests, and the existing link_description, rn_tab_description and timer_description are in the same English-only state until it runs again. Keeping the checked state with TalkBack avoids a regression for non-English users in the meantime (see the discussion on #58739).

The mixed string value keeps its existing handling in BaseViewManager.updateViewContentDescription.

Fixes #58739
Fixes #58588

Changelog:

[ANDROID] [FIXED] - Set stateDescription so TalkBack announces unchecked checkboxes and radios, and no longer reads "checked. on." for switches

Test Plan:

yarn format-check-kotlin                                   # clean
./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest \
  --tests "com.facebook.react.uimanager.ReactAccessibilityDelegateTest"

Result: BUILD SUCCESSFUL, ReactAccessibilityDelegateTest: 22 tests, 0 failures (14 existing + 8 new).

On Windows the Gradle run also needed -Preact.internal.windowsBashPath=..., a pre-built codegen (yarn --cwd packages/react-native-codegen build with -x :packages:react-native:ReactAndroid:buildCodegenCLI) and a checkout path without spaces; none of that is part of this change.

New Robolectric tests in ReactAccessibilityDelegateTest cover: unchecked checkbox, checked checkbox (stateDescription stays unset), unchecked radio, unchecked state with no role, switch on and off via accessibilityRole, switch off via the role prop, and that a state map without checked leaves stateDescription unset.

I was not able to verify on a device with TalkBack; the expected announcements follow the behavior of native CompoundButton, which TalkBack already handles this way.

TalkBack speaks the unchecked state of a checkable node, and avoids
announcing "checked" and "on" together for a switch, only when the node
carries a stateDescription (API 30+). Native CompoundButton sets it, but
ReactAccessibilityDelegate never did, so a React Native checkbox or radio
with checked: false was announced with no state at all, and a switch with
checked: true was read as "checked. on.".

Set stateDescription whenever the checked state is a boolean: "on"/"off"
for the switch role and "checked"/"not checked" otherwise, using the
existing uimanager string resources.

Fixes react#58739
Fixes react#58588
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 12, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 12, 2026
TalkBack already announces the checked state of a checkable node in the
device language. React Native's state strings are translated by Meta's
internal pipeline after import, so a new string would be English-only
for a while. Keep the switch on/off strings, which are already translated
in every locale, and only fill the case TalkBack is silent on: unchecked
checkboxes and radios. Drop the unused state_checked_description.
@BilalWalker

Copy link
Copy Markdown
Author

Pushed cff022c in response to the discussion on #58739: for checkbox/radio, stateDescription is now only set for the unchecked state (new state_unchecked_description), so the checked announcement stays TalkBack's localized one. The switch role keeps the already-translated state_on_description / state_off_description. Dropped the unused state_checked_description. Description updated; ReactAccessibilityDelegateTest 22/22 still passing.

@salrahman

Copy link
Copy Markdown

Thanks @BilalWalker, the narrowed version looks right to me. The checked state keeps TalkBack's own localised wording, the unchecked state finally gets announced, and switches reuse the already-translated on and off strings. For TalkBack users, an English "not checked" is a clear improvement on silence while the translations catch up.

This branch has not been deployed

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

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

2 participants