DEV Community

Chloe
Chloe

Posted on

React Native Accessibility: The 2026 WCAG 2.2 Guide (Every Criterion Every Prop)

TL;DR

  • The legal pressure is real. The DOJ's ADA Title II rule names mobile apps and WCAG 2.1 AA explicitly (deadlines now April 2027 / April 2028). The EU's EAA has been live since June 2025 and points to WCAG 2.2 AA via EN 301 549.
  • React Native maps almost 1:1 to native a11y APIs. accessibilityLabel becomes the iOS accessibility label and the Android content description. You already have the tools.
  • WCAG 2.2 adds four mobile-relevant criteria: Focus Not Obscured, Dragging Movements, Target Size (24×24 min), and Accessible Authentication.
  • Five bugs cause most failures: unlabeled Pressables, modals that don't trap focus, visual-only form errors, custom switches with no state, and low-contrast text on gradients.
  • Test in four layers: ESLint on every commit, a VoiceOver/TalkBack walkthrough on every UI PR, and Accessibility Inspector/Scanner plus axe before every store submission.

Why this suddenly matters

Accessibility used to be the "we'll get to it" ticket. Three things changed that.

1. The DOJ Title II rule. Published in April 2024, it's the first U.S. federal regulation to name mobile apps alongside websites and tie them to a specific standard: WCAG 2.1 AA. In April 2026 the DOJ pushed the deadlines back a year. Public entities with 50,000+ population now have until April 26, 2027, and smaller entities until April 26, 2028. The requirements themselves didn't change. State procurement contracts already cite WCAG 2.1 AA as a pass/fail checkbox, so if you build B2G apps you're in scope today.

2. The European Accessibility Act. In force since June 2025, it covers private-sector products like banking, e-commerce and transit apps. It references EN 301 549, which is harmonized with WCAG 2.2 AA. If you ship to EU users above the small-business threshold, it applies to you.

3. Litigation. Mobile app accessibility lawsuits more than doubled between 2022 and 2025. Courts keep treating apps as places of public accommodation under ADA Title III. Insurers have started asking about accessibility testing before writing cyber-liability policies.

So: you need an a11y audit in your release checklist, run on every store build, with a report you can point to.


The entire React Native a11y toolbox

WCAG 2.2 has 86 success criteria across three conformance levels (A, AA, AAA). For Level AA you need to pass the 55 A and AA criteria, minus a few web-only ones like skip links. Everything else maps to this prop surface:

<Pressable
  accessible={true}
  accessibilityLabel="Save changes"
  accessibilityHint="Saves your draft"
  accessibilityRole="button"
  accessibilityState={{ disabled: false, busy: isSaving }}
  accessibilityValue={{ min: 0, max: 100, now: 42 }}
  importantForAccessibility="yes"        // Android
  accessibilityLiveRegion="polite"        // Android
>
  <Text>Save</Text>
</Pressable>
Enter fullscreen mode Exit fullscreen mode

That's it. The hard part isn't the API. It's knowing which prop satisfies which criterion, and making those props the default so nobody ships a screen without them.

The four criteria new in WCAG 2.2 that hit native UIs:

Criterion Level What it means in an app
2.4.11 Focus Not Obscured (Min) AA Focused element can't be fully hidden by sticky headers, bottom sheets or the keyboard
2.5.7 Dragging Movements AA Every drag gesture needs a non-drag alternative
2.5.8 Target Size (Min) AA Targets at least 24×24 (aim for 44×44 iOS / 48×48 Android)
3.3.8 Accessible Authentication (Min) AA No cognitive tests to log in. Allow paste and password managers

Perceivable + Operable: criterion → code

1.1.1 Non-text Content (A)

Label meaningful images and hide decorative ones.

// Meaningful
<Image source={avatar} accessibilityLabel="Profile photo of Priya" />

// Decorative: hide on both platforms
<Image
  source={divider}
  accessibilityElementsHidden={true}               // iOS
  importantForAccessibility="no-hide-descendants"  // Android
/>
Enter fullscreen mode Exit fullscreen mode

1.3.1 Info and Relationships (A)

A styled View with an onPress is invisible to screen readers as a control. Declare the role.

<Text accessibilityRole="header" style={styles.h1}>Settings</Text>

<Pressable accessibilityRole="link" onPress={openTerms}>
  <Text>Terms of service</Text>
</Pressable>
Enter fullscreen mode Exit fullscreen mode

1.4.3 Contrast (Minimum) (AA)

Body text needs 4.5:1. Large text (18pt+, or 14pt+ bold) needs 3:1. The classic React Native failure is text on a gradient hero that passes at the top and fails at the bottom. Use semantic tokens so a palette change doesn't mean re-auditing every screen:

// theme.js: pairs pre-checked for AA contrast
export const colors = {
  textPrimary: '#1A1A1A',
  surface: '#FFFFFF',
  textOnBrand: '#FFFFFF',
  brand: '#0B5FFF',
};
Enter fullscreen mode Exit fullscreen mode

1.4.4 Resize Text + 1.4.10 Reflow (AA)

Text must scale to 200% without clipping. allowFontScaling is on by default, so leave it alone. Fixed heights are what break:

// ❌ clips at large Dynamic Type sizes
<View style={{ height: 48 }}><Text>{label}</Text></View>

// ✅ grows with the text
<View style={{ minHeight: 48 }}><Text>{label}</Text></View>
Enter fullscreen mode Exit fullscreen mode

2.1.1 Keyboard (A)

Hardware keyboards are a real case on iPads, foldables and Android devices with Bluetooth keyboards. TextInput's onKeyPress covers most of it. Any custom gesture needs an alternative, which also covers 2.5.7 below.

2.4.3 Focus Order (A) + 2.4.11 Focus Not Obscured (AA)

Focus order follows the view hierarchy, which usually matches the visual order. It breaks when you use absolute positioning or zIndex. Modals need to trap focus:

<View
  importantForAccessibility={modalOpen ? 'no-hide-descendants' : 'auto'}
  accessibilityElementsHidden={modalOpen}
>
  {/* background screen */}
</View>

<Modal visible={modalOpen}>
  <View accessibilityViewIsModal={true}>{/* modal content */}</View>
</Modal>
Enter fullscreen mode Exit fullscreen mode

For 2.4.11, keep focused inputs above the keyboard:

<KeyboardAvoidingView
  behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
  style={{ flex: 1 }}
>
  <ScrollView keyboardShouldPersistTaps="handled">{/* form */}</ScrollView>
</KeyboardAvoidingView>
Enter fullscreen mode Exit fullscreen mode

2.5.8 Target Size (AA)

The usual offender is a 16pt icon button with no padding. hitSlop enlarges the touch area without changing the visuals:

<Pressable
  onPress={close}
  hitSlop={{ top: 14, bottom: 14, left: 14, right: 14 }}
  accessibilityRole="button"
  accessibilityLabel="Close"
>
  <Icon name="x" size={16} />
</Pressable>
Enter fullscreen mode Exit fullscreen mode

2.5.7 Dragging Movements (AA)

react-native-draggable-flatlist won't give you an alternative for free. The cleanest RN-native fix is custom accessibility actions, which screen reader users reach through the VoiceOver rotor or the TalkBack actions menu:

<View
  accessible
  accessibilityLabel={`${item.title}, position ${index + 1}`}
  accessibilityActions={[
    { name: 'moveUp', label: 'Move up' },
    { name: 'moveDown', label: 'Move down' },
  ]}
  onAccessibilityAction={({ nativeEvent }) => {
    if (nativeEvent.actionName === 'moveUp') move(index, index - 1);
    if (nativeEvent.actionName === 'moveDown') move(index, index + 1);
  }}
>
  {/* draggable row */}
</View>
Enter fullscreen mode Exit fullscreen mode

Add a visible long-press menu with the same options for sighted users who can't drag.


Understandable + Robust: forms, roles, state

These are the failures that get apps sued, because they stop people from completing a transaction.

3.3.1 Error Identification + 3.3.3 Error Suggestion

A red border means nothing to a screen reader. Announce the error:

import { AccessibilityInfo } from 'react-native';

function onSubmit() {
  if (!email) {
    setError('Email is required');
    AccessibilityInfo.announceForAccessibility('Email is required');
    return;
  }
}

<TextInput
  accessibilityLabel={error ? `Email, error: ${error}` : 'Email'}
  value={email}
  onChangeText={setEmail}
/>
Enter fullscreen mode Exit fullscreen mode

3.3.2 Labels or Instructions (A)

Placeholder-only inputs fail because the placeholder disappears when you type. Use a persistent label. On Android you can link the two with accessibilityLabelledBy + nativeID:

<Text nativeID="emailLabel">Email</Text>
<TextInput
  accessibilityLabel="Email"            // iOS
  accessibilityLabelledBy="emailLabel"  // Android
/>
Enter fullscreen mode Exit fullscreen mode

3.3.8 Accessible Authentication (AA, new in 2.2)

Let password managers do their job, and never block paste:

<TextInput
  secureTextEntry
  textContentType="password"   // iOS
  autoComplete="password"      // Android + iOS
  contextMenuHidden={false}    // keep paste available
/>
Enter fullscreen mode Exit fullscreen mode

4.1.2 Name, Role, Value (A)

Custom controls need all three. Miss one and the control fails:

<Pressable
  onPress={() => setEnabled(!enabled)}
  accessibilityRole="switch"
  accessibilityLabel="Dark mode"
  accessibilityState={{ checked: enabled }}
>
  {/* visual toggle */}
</Pressable>
Enter fullscreen mode Exit fullscreen mode

VoiceOver reads it as "Dark mode, switch, on." 👌

4.1.3 Status Messages (AA)

Toasts and "Saved" confirmations should be announced without moving focus:

<Text accessibilityLiveRegion="polite">{statusMessage}</Text> // Android

useEffect(() => {
  if (statusMessage) AccessibilityInfo.announceForAccessibility(statusMessage); // iOS + Android
}, [statusMessage]);
Enter fullscreen mode Exit fullscreen mode

The 4-layer testing workflow

You can't claim compliance you can't demonstrate.

Layer 1: Lint on every commit.

npm i -D eslint-plugin-react-native-a11y
Enter fullscreen mode Exit fullscreen mode
{
  "plugins": ["react-native-a11y"],
  "extends": ["plugin:react-native-a11y/all"]
}
Enter fullscreen mode Exit fullscreen mode

This catches missing labels, invalid roles and unlabeled inputs. It won't catch contrast or focus order, and no linter can.

Layer 2: Screen reader walkthrough on every UI PR. Turn on VoiceOver (Settings → Accessibility → VoiceOver) or TalkBack, close your eyes, and complete each primary flow. It takes about 20 minutes per flow and catches more real bugs than anything else on this list.

Layer 3: Platform auditors before release. On iOS, use Xcode → Open Developer Tool → Accessibility Inspector. On Android, use Google's Accessibility Scanner. Both flag contrast, missing labels and small targets.

Layer 4: Automated regression. axe DevTools Mobile plugs into Appium and runs WCAG 2.2 rulesets against built apps. It's pricey, but it's how you prove compliance at CI speed on a big app.

If you ship weekly, that's roughly a 2-hour a11y pass per release.


The 5 bugs I see in almost every RN audit

# Bug Fix
1 Icon-only Pressable with no label accessibilityLabel + accessibilityRole="button"
2 Modal doesn't trap focus accessibilityViewIsModal (iOS) + hide background (Android)
3 Form errors are visual only AccessibilityInfo.announceForAccessibility()
4 Custom switch reads as "button" accessibilityRole="switch" + accessibilityState={{ checked }}
5 Low contrast on gradients Semi-opaque overlay behind text, or move the text

Bugs 1–4 are all "forgot a prop" bugs, which makes them a defaults problem more than a skills problem. Your component library and codegen should require these props on interactive elements so the accessible path is the easy path. Some generators already work this way. RapidNative, for example, outputs screens with accessibilityRole, accessibilityLabel, accessibilityState and hitSlop already filled in. Contrast (bug 5) is harder to automate because it depends on your palette, but AA-checked semantic tokens get you most of the way.


Build it in, don't bolt it on

Retrofitting a 40-screen app that shipped without a11y props takes 2–4 weeks of focused work. Building those props in from day one costs close to nothing. The cheapest fix is upstream:

  • Make accessibilityLabel and accessibilityRole required in your design-system components (TypeScript helps here).
  • Turn on eslint-plugin-react-native-a11y with errors, not warnings.
  • Ship AA-safe color tokens and hitSlop defaults in your base Button and IconButton.

Then do the one thing that beats every tool: spend 20 minutes with VoiceOver on your current build.

What's the worst a11y bug you've found in your own app? Drop it in the comments 👇

Top comments (0)