DEV Community

alican akyol
alican akyol

Posted on

3,968 buttons a screen reader can't name: accessibility in 871 open-source React Native apps

Turn on VoiceOver, open a React Native app, and swipe through the header. Quite often you hear "button", then "button", then "button". Those are the close, back and menu icons. Nothing says which is which.

I added accessibility checks to NativeKeel, the free CLI I build for React Native and Expo health checks, and ran them on the 871 open-source apps I use to test it (14 October 2026, NativeKeel 0.1.77).

Apps
At least one accessibility finding 418 (48%)
Icon-only buttons without a label 282
Buttons smaller than 44×44 with no hitSlop 209
Text or placeholders below WCAG contrast 188
Text fields without a label or placeholder 119
Three or more of these 115

Every check only reports what it can see for certain in the source. That's why some numbers are lower than you might expect: when a color comes from a theme, or a button is a custom component, the check stays quiet instead of guessing. I read samples of each by hand before publishing the numbers.

1. Icon-only buttons with no name (282 apps, 3,968 buttons)

<Pressable onPress={onClose}>
  <Ionicons name="close" size={24} />
</Pressable>
Enter fullscreen mode Exit fullscreen mode

The touchable is accessible, so VoiceOver stops on it. But there's no text inside, only an icon font glyph or an image, so there's nothing to read. The user hears "button".

<Pressable onPress={onClose} accessibilityLabel="Close" accessibilityRole="button">
  <Ionicons name="close" size={24} />
</Pressable>
Enter fullscreen mode Exit fullscreen mode

The label describes the action ("Close", "Delete photo"), not the icon ("X").

A related gap is much more common: Pressable and TouchableOpacity don't set a role. Without accessibilityRole="button" the user hears the text but not that it can be pressed. 94% of the apps with touchables miss it somewhere. NativeKeel lists it as a note rather than a warning, because a warning that appears in nearly every report stops meaning anything. The cheap fix is to set it once in your own Button component.

2. Buttons too small to hit (209 apps)

const styles = StyleSheet.create({
  closeButton: { width: 28, height: 28, borderRadius: 14 },
});
Enter fullscreen mode Exit fullscreen mode

Apple asks for at least 44×44 points and Android for 48×48 dp. A 28-point circle is easy to miss for someone with a tremor, or for anyone on a bus. hitSlop makes the touch area bigger without changing the look:

<Pressable style={styles.closeButton} hitSlop={10} onPress={onClose} />
Enter fullscreen mode Exit fullscreen mode

The check only uses sizes it can read: an explicit width and height in an inline style or the same file's StyleSheet, or no style at all around a single small icon. When a button is also under 24×24, the report says so separately, because that is the WCAG 2.2 AA minimum. A 40×40 button meets WCAG and still misses the platform guidelines, and the report doesn't call it a WCAG failure.

3. Light grey on white (188 apps)

The median pair I found was 2.85:1. WCAG AA asks for 4.5:1 for normal text and 3:1 for large text. The usual suspects:

Text Background Ratio
#bbb #fff 1.92
#fff #4caf50 (Material green) 2.78
#999 #fff 2.85
#808080 #fff 3.95

Placeholders count too: people read them to know what to type. 226 placeholders in 55 apps are below 4.5:1, mostly placeholderTextColor="#999" on a white screen. A TextInput is transparent by default, so the background is whatever view is behind it.

The check walks up from the Text to the nearest view with a background color. It gives up at anything it can't see through: an image background, a gradient, opacity, a theme color, a custom component. So the real number of low-contrast apps is higher, and the ones it reports are pairs you can verify with any contrast checker.

4. Text fields with no name (119 apps)

<Text style={styles.label}>Email</Text>
<TextInput style={styles.input} value={email} onChangeText={setEmail} />
Enter fullscreen mode Exit fullscreen mode

A sighted user reads "Email" above the field. VoiceOver says "text field". The visible label isn't linked to the input: accessibilityLabelledBy exists, but only on Android. Adding accessibilityLabel="Email" to the input fixes both platforms.

The React 19 trap: Text.defaultProps stops working

This one isn't strictly accessibility, but I found it while looking at text scaling. Some apps turn off font scaling for the whole app like this:

Text.defaultProps = Text.defaultProps || {};
Text.defaultProps.allowFontScaling = false;
Enter fullscreen mode Exit fullscreen mode

On React 19, which ships with React Native 0.78 and later, that line does nothing for <Text> in your JSX. React 19 applies defaultProps only to class components. React Native's Text is a function component, and React Native and Expo compile JSX with the automatic runtime, whose jsx() doesn't read defaultProps at all. I checked this in the React Native 0.82 renderer (resolveClassComponentProps is the only place defaults are merged) and in the React 19 JSX runtime.

What changes for the app is that the global font, color or scaling limit silently stops applying when you upgrade past 0.77. No warning, no error. If you capped scaling to protect a fixed layout, text now follows the system size and the layout can break. Move these defaults into your own Text component before the upgrade. (Only components rendered through React.createElement, as NativeWind does, still see defaultProps, so NativeKeel doesn't flag NativeWind projects.)

What static checks can't tell you

They can't hear your app. Focus order, custom components that forward labels, what a screen reader actually announces after a navigation, dynamic type in a long list: those need a device. The checks above are the part you can fix before anyone turns on VoiceOver. Then do one pass of your main flows with VoiceOver, one with TalkBack, and one at the largest text size.

If you sell in the EU, this is no longer optional for everyone: the European Accessibility Act has applied since 28 June 2025 to services such as e-commerce and consumer banking, apps included.

Try it

npx nativekeel           # accessibility findings are in the normal report
npx nativekeel improve   # an ordered plan, with an accessibility group
Enter fullscreen mode Exit fullscreen mode

It reads files only, doesn't install or run your app, and your code doesn't leave your machine. If a finding is wrong for your app, please tell me: false alarms are what I fix first. github.com/AlicanAkyol/nativekeel

Top comments (0)