DEV Community

Mason Roy
Mason Roy

Posted on Originally published at Medium

I Audited 12 React Native Apps for WCAG 2.2. Here's What 11 of Them Got Wrong.

TL;DR

  • I audited 12 production React Native apps against WCAG 2.2 AA. 11 would fail. The median app failed 23 of 55 criteria.
  • The DOJ's ADA Title II deadline for state and local government apps is now April 2027 (pushed back a year in April 2026). The EU Accessibility Act is already in force for private-sector apps.
  • RN maps almost 1:1 to native accessibility APIs. The problem is nothing forces you to use them.
  • Retrofitting a 40-screen app takes 2–4 engineer-weeks. Building with the props from day one costs ~zero.
  • Fastest wins: test with VoiceOver/TalkBack, add eslint-plugin-react-native-a11y as a merge blocker, fix the five patterns below.

The problem isn't React Native

Every accessibilityLabel becomes a UIAccessibilityLabel on iOS. Every accessibilityRole ends up in AccessibilityNodeInfo on Android. The plumbing is good.

But you can ship an entire app of styled Views with onPress handlers and never set a single accessibility prop. It runs, it looks fine, and VoiceOver users can't see any of it.

All 12 apps I audited had the same five patterns. Here they are, with fixes.

1. Custom buttons with no label or role

The classic:

// ❌ VoiceOver reads nothing useful
<Pressable onPress={onClose}>
  <CloseIcon />
</Pressable>
Enter fullscreen mode Exit fullscreen mode
// ✅ Announced as "Close, button"
<Pressable
  onPress={onClose}
  accessibilityRole="button"
  accessibilityLabel="Close"
  accessibilityHint="Closes the settings panel"
>
  <CloseIcon />
</Pressable>
Enter fullscreen mode Exit fullscreen mode

Icon-only buttons are the worst offenders. If there's no visible text, there must be a label.

2. Tiny touch targets

WCAG 2.2 added Target Size (Minimum) at AA: at least 24×24 CSS px. Apple and Google both recommend going bigger (44pt / 48dp). A 16pt icon fails all of them.

You don't have to change the visual size, just the hit area:

<Pressable
  onPress={onLike}
  accessibilityRole="button"
  accessibilityLabel="Like"
  hitSlop={{ top: 14, bottom: 14, left: 14, right: 14 }}
>
  <HeartIcon size={16} />
</Pressable>
Enter fullscreen mode Exit fullscreen mode

3. Form errors that are only red text

A sighted user sees the error. A screen reader user hears nothing and wonders why submit didn't work.

import { useEffect } from 'react';
import { AccessibilityInfo, Text, TextInput } from 'react-native';

function EmailField({ value, onChange, error }) {
  useEffect(() => {
    if (error) AccessibilityInfo.announceForAccessibility(error);
  }, [error]);

  return (
    <>
      <TextInput
        value={value}
        onChangeText={onChange}
        accessibilityLabel="Email address"
        accessibilityHint={error ?? undefined}
      />
      {error && (
        <Text accessibilityLiveRegion="polite" style={{ color: '#B00020' }}>
          {error}
        </Text>
      )}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

accessibilityLiveRegion covers Android; announceForAccessibility covers iOS.

4. Modals that don't trap focus

Users swipe right past your dialog into the screen behind it. Mark the modal content as modal on iOS and hide the background on Android:

<View accessibilityViewIsModal>
  {/* dialog content */}
</View>

<View importantForAccessibility={modalOpen ? 'no-hide-descendants' : 'auto'}>
  {/* screen behind the modal */}
</View>
Enter fullscreen mode Exit fullscreen mode

React Native's built-in <Modal> handles most of this for you. Custom bottom sheets and overlays usually don't.

5. Text on gradients

Text that passes 4.5:1 contrast at the top of a gradient often fails at the bottom. Check the lightest point the text sits on, not the average. The quick fix is a solid or semi-opaque scrim behind the text.

Stop new violations at the PR

None of the fixes above matter if new screens keep shipping without props. Add the lint plugin and make it a required check:

npm install --save-dev eslint-plugin-react-native-a11y
Enter fullscreen mode Exit fullscreen mode
// .eslintrc.js
module.exports = {
  plugins: ['react-native-a11y'],
  extends: ['plugin:react-native-a11y/all'],
};
Enter fullscreen mode Exit fullscreen mode

It won't catch contrast or focus order, but in my audits it would have caught roughly 90% of failures: the missing labels and roles.

Retrofit vs. restart

The retrofit math: a 40-screen app built without accessibility takes 2–4 weeks of one engineer doing nothing else. With 200 screens, every fix needs review, QA and regression testing.

If your app is under six months old and under thirty screens, restarting on a stack that generates accessible components by default can be cheaper than retrofitting. That's part of why we built RapidNative: every generated screen ships with accessibility props filled in, because the templates treat them as required.

The 20-minute test

Before any of this: turn on VoiceOver (iOS) or TalkBack (Android), close your eyes, and try your app's main user flow. Not a demo — the real thing. If you can't finish it, neither can your users.

That one exercise will tell you more about your priorities than any audit report.


What's the worst accessibility bug you've found in your own RN app? Drop it in the comments. I'm collecting them for a follow-up post.

Top comments (0)