Repository navigation
aria-* and role are silently dropped by Switch, ScrollView, ActivityIndicator and Touchable* #59377
Description
Description
aria-*, role, id and tabIndex are typed on every core component through AccessibilityProps / ViewProps, and the docs of Switch, ScrollView and ActivityIndicator say they inherit the View props. The native side does not read the web-style names: the Fabric props parser and the Android view managers read accessibility*, nativeID and focusable, and of the web-style props only role. The conversion lives in each JS component (View.js, Text.js, TextInput.js, Image.*.js, Pressable, Touchable*), and it is missing in two ways:
Switch,ScrollView,ActivityIndicator,RefreshControlandModalrender a native component directly and convert nothing, so everyaria-*prop reaches the native side unconverted and is dropped.Touchable*copy a fixed list of props to theirView.roleis on none of the lists,TouchableWithoutFeedbackleaves outaria-label, andTouchableHighlightleaves out thearia-*state props.
Nothing warns: the types accept the props, and the native view configs list the raw aria-* as valid attributes.
The gaps were found by reading main (5709e80); the reproducer below confirms the rows users hit, on Android and iOS.
| Component | Dropped | Covered by an open PR |
|---|---|---|
Switch, ScrollView, ActivityIndicator, RefreshControl, Modal |
every aria-* (aria-label, aria-hidden, aria-disabled, aria-checked, …), id, tabIndex |
aria-hidden on ScrollView and ActivityIndicator: #59072 |
TouchableOpacity, TouchableNativeFeedback |
role, aria-labelledby |
aria-labelledby: #58173 |
TouchableHighlight |
role, aria-labelledby, aria-busy, aria-checked, aria-disabled, aria-expanded, aria-selected |
aria-labelledby: #58173 (not this component) |
TouchableWithoutFeedback |
role, aria-label, aria-labelledby |
— |
Text |
aria-labelledby, aria-modal, aria-value* |
— (aria-live fixed in #59330) |
TextInput, Image |
aria-live, aria-modal, aria-value* |
— |
View |
aria-modal |
#59328 |
Some of these make no sense on the component (aria-modal on Image, aria-live on Switch); the ones users hit are aria-label on Switch (and on ActivityIndicator on Android), role on Touchable*, aria-label on TouchableWithoutFeedback and aria-checked on TouchableHighlight.
The same class of bug has been fixed one component at a time: #53051, #53364 and #59330 are merged, #59072, #58173 and #59328 are open. The shared Fantom suite accessibilityPropsSuite covers only the accessibility* props, and rolePropSuite runs only for Text, so the tests do not catch it.
Related: aria-labelledby is a space-separated id list on the web; View splits it by commas, TextInput and Image pass it as one id, and Android uses only the first id. A web-style aria-labelledby="title hint" matches nothing.
Questions for the maintainers:
- Is a shared JS helper (the conversion
View.jsdoes now, reused by the components that render a native component directly) the preferred fix? Native parsing existed behindenableNativeViewPropTransformationsand was removed in Remove native View prop transformation flag (#56699) #56699; was that a decision against native parsing? - Should the shared Fantom suites get the
aria-*androlecases for every component, so new gaps fail in CI?
Fixes:
- fix(Touchable): pass role and the missing aria props to the host view #59378,
Touchable*: passrole, maparia-labelinTouchableWithoutFeedbackand the state props inTouchableHighlight, and runrolePropSuitewhere Fantom can seerole. This one does not depend on the questions above. - fix: map aria-* props in Switch, ScrollView and ActivityIndicator #59379,
Switch,ScrollViewandActivityIndicator: a shared helper with the conversionView.jsdoes, my proposal for question 1. It also mapsaria-hidden, which overlaps with Hide ScrollView and ActivityIndicator content when aria-hidden is set #59072 (cc @YevheniiKotyrlo); I am happy to rebase on it or drop that part.
Steps to reproduce
- Run the reproducer from
ReproducerApp:yarn install, thenyarn androidoryarn ios(an emulator or a simulator is enough). - Android:
adb shell uiautomator dump /sdcard/ui.xml && adb pull /sdcard/ui.xml. iOS: Accessibility Inspector. - Compare each pair: the left element uses the web-style prop, the right one the
accessibility*form.
Expected: both elements of a pair expose the same label, role and checked state.
Actual, from ui.xml on an Android 17 emulator:
| Pair | Web-style prop | accessibility* form |
|---|---|---|
Switch A / B |
content-desc="", NAF="true" |
content-desc="Wi-Fi B" |
ActivityIndicator C / D |
content-desc="" |
content-desc="Loading D" |
ScrollView E / F |
content-desc="" |
content-desc="Photos F" |
TouchableOpacity G / H |
class="android.view.ViewGroup" |
class="android.widget.Button" |
TouchableWithoutFeedback I / J |
content-desc="×" (the child's text) |
content-desc="Close J" |
TouchableHighlight K / L |
checkable="false" checked="false" |
checkable="true" checked="true" |
Actual, from Accessibility Inspector on an iOS 27.0 simulator (iPhone 18 Pro):
| Pair | Web-style prop | accessibility* form |
|---|---|---|
Switch A / B |
Label: none | Label: "Wi-Fi B" |
TouchableOpacity G / H |
Traits: none | Traits: Button |
TouchableWithoutFeedback I / J |
Label: "×" | Label: "Close J" |
TouchableHighlight K / L |
Value: "checkbox" | Value: "checkbox, checked" |
Two pairs do not apply on iOS:
ScrollViewE / F: the scroll view is not an accessibility element, so neither label is exposed.ActivityIndicatorC / D: both read "In progress", the label of the innerUIActivityIndicatorView.accessibilityLabelgoes to the host view, which is not an accessibility element, and the indicator is added as a plain subview (RCTActivityIndicatorViewComponentView.mm). Withaccessibleadded, the host is read as "Loading D" but loses the indicator's value.RCTSwitchComponentViewhandles the same case by returning the inner view fromaccessibilityElement. This is a separate bug: theaccessibility*form is ignored too, so thearia-*fix does not change it. I will file it separately.
React Native Version
0.88.0
Affected Platforms
Runtime - Android, Runtime - iOS
Output of npx @react-native-community/cli info
System:
OS: macOS 27.0.1
CPU: (10) arm64 Apple M5
Memory: 176.09 MB / 16.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 24.15.0
Yarn:
version: 1.22.22
npm:
version: 11.12.1
Watchman:
version: 2026.07.20.00
Managers:
CocoaPods:
version: 1.17.0
SDKs:
iOS SDK:
Platforms:
- DriverKit 27.0
- iOS 27.1
- macOS 27.0
- tvOS 27.0
- visionOS 27.0
- watchOS 27.0
Android SDK: Not Found
IDEs:
Android Studio: 2026.2 AI-262.9437.185.2621.16467767
Xcode:
version: 27.1/27A9275
Languages:
Java:
version: 17.0.20
Ruby:
version: 4.0.7
npmPackages:
"@react-native-community/cli":
installed: 20.2.0
wanted: 20.2.0
react:
installed: 19.3.0
wanted: 19.3.0
react-native:
installed: 0.88.0
wanted: 0.88.0
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: true
newArchEnabled: true
Stacktrace or Logs
No crash or warning
MANDATORY Reproducer
https://github.com/faciledictu/rn-aria-props-repro
Screenshots and Videos
No response
Activity