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>
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>
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 },
});
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} />
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} />
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;
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
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)