DEV Community

Cover image for React Native Accessibility: The 2026 WCAG 2.2 Compliance Guide
Hugo Rus
Hugo Rus

Posted on

React Native Accessibility: The 2026 WCAG 2.2 Compliance Guide

Most React Native apps fail accessibility audits for the same dozen reasons, and almost none of them are hard to fix. An unlabeled icon button. A tap target that's 28 points wide. A modal that VoiceOver can read straight through. Text that clips at 200% font size.

This guide maps WCAG 2.2 Level AA to the React Native APIs that satisfy it, with code for each. If you build to WCAG 2.2 AA, you also meet WCAG 2.1 AA, because 2.2 is backwards compatible and only adds criteria (plus removes one obsolete one, 4.1.1 Parsing).

Not legal advice. If you're building for a government agency, a public university, or a healthcare provider, confirm your specific obligations with counsel.

Why this matters in 2026

The U.S. Department of Justice's ADA Title II rule makes WCAG 2.1 Level AA the technical standard for web content and mobile apps provided by state and local governments, including public schools, universities, courts, transit authorities, and public hospitals. That includes apps built for them by contractors.

The deadlines moved. On April 20, 2026, four days before the original deadline, DOJ published an interim final rule extending them by a year:

Public entity Original deadline Current deadline
Population 50,000 or more April 24, 2026 April 26, 2027
Population under 50,000, and special districts April 26, 2027 April 26, 2028

Two things the extension didn't change. First, it only delays the WCAG compliance dates; Title II's underlying nondiscrimination obligations still apply today, and those have supported accessibility lawsuits for years. Second, the separate HHS Section 504 rule for recipients of HHS funding kept its May 11, 2026 first deadline.

And outside the U.S., the European Accessibility Act has applied to many consumer-facing apps (banking, e-commerce, transport, e-books) since June 28, 2025.

Even without a legal requirement, roughly one in six people worldwide lives with a significant disability. Accessibility is a big chunk of your addressable market.

1. Label everything a screen reader can focus

WCAG 1.1.1 Non-text Content (A), 4.1.2 Name, Role, Value (A)

The single most common failure: icon-only buttons with no label. VoiceOver reads "button." TalkBack reads "unlabeled."

// ❌ Screen reader says: "button"
<Pressable onPress={onDelete}>
  <TrashIcon />
</Pressable>

// ✅ Screen reader says: "Delete note, button. Removes this note permanently."
<Pressable
  onPress={onDelete}
  accessibilityRole="button"
  accessibilityLabel="Delete note"
  accessibilityHint="Removes this note permanently"
>
  <TrashIcon />
</Pressable>
Enter fullscreen mode Exit fullscreen mode

Rules of thumb:

  • Labels describe what it is, not what it looks like. "Delete note," not "trash can icon."
  • Don't put the role in the label. "Delete button" becomes "Delete button, button."
  • Hints are optional and describe the result. Use them only when the label isn't enough.
  • Decorative images should be hidden: accessible={false} on the Image, or wrap them in a parent that sets importantForAccessibility="no" on Android.

Since React Native 0.71 you can also use the web-style aria-* props and role, which map to the same native properties:

<Pressable role="button" aria-label="Delete note" onPress={onDelete}>
  <TrashIcon />
</Pressable>
Enter fullscreen mode Exit fullscreen mode

Pick one style per codebase and lint for it.

2. Expose state, not just labels

WCAG 4.1.2 Name, Role, Value (A)

A checkbox that looks checked but announces nothing about its state fails, even with a perfect label.

<Pressable
  accessibilityRole="checkbox"
  accessibilityState={{ checked: isDone }}
  accessibilityLabel="Buy groceries"
  onPress={toggle}
>
  <Checkbox value={isDone} />
  <Text>Buy groceries</Text>
</Pressable>
Enter fullscreen mode Exit fullscreen mode

Other states worth wiring up: selected for tabs and segmented controls, expanded for accordions, disabled for inactive buttons, and busy while something loads.

For sliders and progress bars, use accessibilityValue:

<View
  accessibilityRole="adjustable"
  accessibilityLabel="Volume"
  accessibilityValue={{ min: 0, max: 100, now: volume }}
  accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
  onAccessibilityAction={(e) =>
    setVolume((v) => (e.nativeEvent.actionName === 'increment' ? v + 10 : v - 10))
  }
/>
Enter fullscreen mode Exit fullscreen mode

Without accessibilityActions, screen reader users can't change the value at all; swipe up and down on an adjustable element does nothing.

3. Group related content

WCAG 1.3.1 Info and Relationships (A)

A product card with an image, title, price, and rating becomes five separate swipe stops by default. Group it so it reads as one sentence:

<Pressable
  accessible
  accessibilityRole="button"
  accessibilityLabel={`${name}, ${price}, rated ${rating} out of 5`}
  onPress={openProduct}
>
  <Image source={{ uri: photo }} />
  <Text>{name}</Text>
  <Text>{price}</Text>
  <StarRating value={rating} />
</Pressable>
Enter fullscreen mode Exit fullscreen mode

Setting accessible on the parent makes it a single focusable element. The label you write replaces the children's text, so make sure it includes everything that matters.

4. Make tap targets big enough

WCAG 2.5.8 Target Size (Minimum), new in 2.2 (AA)

WCAG 2.2 requires targets of at least 24×24 CSS pixels, with spacing exceptions. That's the legal floor. Apple's Human Interface Guidelines recommend 44×44 points and Material Design recommends 48×48 dp; aim for those.

You don't have to make the icon bigger. Use hitSlop to expand the touchable area:

<Pressable
  onPress={onClose}
  accessibilityRole="button"
  accessibilityLabel="Close"
  hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}   // 20pt icon → 44pt target
>
  <CloseIcon size={20} />
</Pressable>
Enter fullscreen mode Exit fullscreen mode

Watch for adjacent targets: two 24-point icons touching each other technically pass the size rule but are still hard to hit. Leave space between them.

5. Provide alternatives to dragging

WCAG 2.5.7 Dragging Movements, new in 2.2 (AA)

If a feature only works by dragging (reordering a list, a swipe-to-delete row, a range slider), there must be a single-pointer alternative. Typical fixes:

  • Swipe-to-delete → also show a delete action in a long-press menu or an edit mode.
  • Drag-to-reorder → add "Move up" and "Move down" buttons, or expose them as accessibility actions:
<View
  accessible
  accessibilityLabel={item.title}
  accessibilityActions={[
    { name: 'moveUp', label: 'Move up' },
    { name: 'moveDown', label: 'Move down' },
  ]}
  onAccessibilityAction={(e) => move(item.id, e.nativeEvent.actionName)}
/>
Enter fullscreen mode Exit fullscreen mode

Screen reader users reach these through the actions rotor on iOS and the actions menu on Android.

6. Keep focus visible and unobscured

WCAG 2.4.7 Focus Visible (AA), 2.4.11 Focus Not Obscured (Minimum), new in 2.2 (AA)

Mobile users navigate with keyboards, switch control, and external devices more than most developers assume. The rule in 2.2: when an element receives focus, it can't be completely hidden behind something else, like a sticky footer, a floating action button, or a cookie banner.

In practice:

  • Don't remove focus indicators on TextInput and custom buttons.
  • Pad scroll views so the last focusable item can scroll clear of a sticky bottom bar (contentContainerStyle={{ paddingBottom: footerHeight }}).
  • Test with a hardware keyboard on iPad and with Switch Access on Android.

7. Trap focus in modals

WCAG 2.4.3 Focus Order (A)

When a modal opens, screen reader focus should move into it and stay there. React Native's built-in Modal handles this on both platforms. Custom overlays (a bottom sheet built from an absolutely positioned View) usually don't.

For custom overlays, hide everything behind them:

<View
  importantForAccessibility={sheetOpen ? 'no-hide-descendants' : 'auto'}  // Android
  accessibilityElementsHidden={sheetOpen}                                  // iOS
>
  <MainContent />
</View>
{sheetOpen && (
  <View accessibilityViewIsModal>   {/* iOS: ignore siblings */}
    <BottomSheetContent />
  </View>
)}
Enter fullscreen mode Exit fullscreen mode

Then move focus to the sheet's heading when it opens:

const headingRef = useRef(null);

useEffect(() => {
  if (sheetOpen && headingRef.current) {
    const tag = findNodeHandle(headingRef.current);
    if (tag) AccessibilityInfo.setAccessibilityFocus(tag);
  }
}, [sheetOpen]);
Enter fullscreen mode Exit fullscreen mode

8. Announce changes nobody can see

WCAG 4.1.3 Status Messages (AA)

"Item added to cart," "Saved," "3 results found." Sighted users see a toast; screen reader users hear nothing unless you announce it.

import { AccessibilityInfo } from 'react-native';

const addToCart = async () => {
  await api.addToCart(id);
  AccessibilityInfo.announceForAccessibility('Added to cart');
};
Enter fullscreen mode Exit fullscreen mode

On Android you can also mark a region as live so updates are read automatically:

<Text accessibilityLiveRegion="polite">{resultCount} results</Text>
Enter fullscreen mode Exit fullscreen mode

9. Support text scaling to 200%

WCAG 1.4.4 Resize Text (AA), 1.4.10 Reflow (AA)

React Native scales Text with the system font size by default. The failures come from layouts that can't handle it: fixed heights, single-line rows, and text that clips.

  • Don't disable scaling with allowFontScaling={false}. If a specific element genuinely can't grow (a tab bar label), cap it with maxFontSizeMultiplier={1.5} instead.
  • Avoid fixed heights on containers that hold text. Use minHeight.
  • Let rows wrap. flexWrap: 'wrap' or a column layout at large sizes.
  • Test at the largest accessibility size, not just the largest standard size. On iOS that's Settings → Accessibility → Display & Text Size → Larger Text.

10. Meet contrast minimums

WCAG 1.4.3 Contrast Minimum (AA), 1.4.11 Non-text Contrast (AA)

  • 4.5:1 for body text
  • 3:1 for large text (18pt, or 14pt bold) and for UI components like input borders, icons, and focus indicators

The usual offenders are light grey placeholder text, disabled-looking secondary buttons that aren't actually disabled, and thin icon strokes on colored backgrounds. Check both light and dark mode; a palette that passes in one often fails in the other.

Also: don't use color alone to convey meaning (1.4.1). An error field needs an icon or text, not just a red border.

11. Respect reduced motion

WCAG 2.3.3 Animation from Interactions (AAA, but widely expected)

import { AccessibilityInfo } from 'react-native';

const [reduceMotion, setReduceMotion] = useState(false);

useEffect(() => {
  AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);
  const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduceMotion);
  return () => sub.remove();
}, []);
Enter fullscreen mode Exit fullscreen mode

If you use Reanimated, useReducedMotion() does the same in one line. When reduce motion is on, replace slides and parallax with fades or instant transitions.

12. Don't make people re-type or memorize

WCAG 3.3.7 Redundant Entry (A), 3.3.8 Accessible Authentication (Minimum) (AA), both new in 2.2

  • Redundant Entry: if a user already entered their address in step 1 of checkout, prefill it in step 3 or offer "same as shipping."
  • Accessible Authentication: don't block password managers or paste. Set the right content types so autofill works:
<TextInput
  textContentType="emailAddress"      // iOS
  autoComplete="email"                // Android
  keyboardType="email-address"
/>
<TextInput
  textContentType="password"
  autoComplete="password"
  secureTextEntry
/>
<TextInput
  textContentType="oneTimeCode"       // iOS SMS code autofill
  autoComplete="sms-otp"              // Android
/>
Enter fullscreen mode Exit fullscreen mode

Avoid CAPTCHAs that require transcribing distorted text. Passkeys, magic links, and biometrics all satisfy this criterion.

13. Keep help in the same place

WCAG 3.2.6 Consistent Help, new in 2.2 (A)

If your app offers help (a chat button, a support link, a phone number), put it in the same relative place on every screen that has it. A help icon in the header on one screen and in a settings submenu on another fails.

Testing: what to actually do

Automated tools catch maybe a third of issues. The rest needs a human with a screen reader.

Lint in CI. eslint-plugin-react-native-a11y flags missing roles, labels, and invalid states:

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

Test by role in unit tests. React Native Testing Library queries by role and label, so a test that can't find the button is also a screen reader that can't:

const deleteBtn = screen.getByRole('button', { name: 'Delete note' });
fireEvent.press(deleteBtn);
Enter fullscreen mode Exit fullscreen mode

Run the platform tools.

  • iOS: Accessibility Inspector in Xcode (audit any simulator screen)
  • Android: Accessibility Scanner app from Google Play

Test manually, once per release, on real devices:

  • [ ] Navigate every core flow with VoiceOver (iOS) and TalkBack (Android), eyes closed
  • [ ] Every interactive element has a sensible label and role
  • [ ] Modals and sheets trap focus; closing returns focus to the trigger
  • [ ] Status changes are announced
  • [ ] Largest text size: nothing clips, overlaps, or disappears
  • [ ] Contrast passes in both light and dark mode
  • [ ] Every drag interaction has a tap alternative
  • [ ] Reduce motion is respected
  • [ ] Password managers and SMS code autofill work

Start with the 20% that fixes 80%

If you only have a day, do these in order:

  1. Label every icon-only button.
  2. Add hitSlop to every small target.
  3. Fix text clipping at the largest font size.
  4. Add accessibilityState to every checkbox, switch, and tab.
  5. Announce toasts with announceForAccessibility.

That clears the majority of failures in a typical audit.

Building it in from the start

Retrofitting accessibility is far more expensive than starting with it. If you're generating new screens, RapidNative produces React Native and Expo code you own, so you can add the patterns above, lint for them, and keep them consistent as the app grows.

What's the accessibility bug you see most often in React Native apps? Mine is still the unlabeled icon button, every single time.

Top comments (0)