Skip to content
Blog | sirlisko | Luca Lischetti Blog | sirlisko | Luca Lischetti Blog | sirlisko | Luca Lischetti
Go back

Accessibility in React Native: Beyond the Checklist

This post is part of A Year Building a React Native App with Expo, a retrospective series on the decisions and patterns that shaped a production React Native app.

Introduction

Accessibility in mobile apps tends to fall into one of two categories: either it’s treated as a compliance checkbox at the end of a project, or it’s ignored entirely until someone raises it. Both approaches produce the same result: an app that technically works but is unusable for a significant portion of your audience.

Over the past year, I worked on a React Native app where accessibility was part of building each feature, not a separate workstream. The app serves users planning for retirement, a demographic that skews older and is more likely to rely on larger text, screen readers, and high-contrast settings. Getting accessibility wrong wasn’t just a nice-to-have concern; it meant locking out the people the app was built for.

This post covers the practical side: how to think about accessibility in React Native, how to test it, and the specific problems I ran into and solved.

The Three Pillars

Accessibility in React Native broadly comes down to three things:

1. Screen Reader Support

VoiceOver (iOS) and TalkBack (Android) need to be able to navigate your app meaningfully. This means:

One problem I ran into early was with carousels. The app had a custom carousel where off-screen slides were still in the accessibility tree. A VoiceOver user would hear content from slides they couldn’t see, with no way to know it was off-screen. The fix was marking non-visible carousel items as inaccessible to screen readers:

accessibilityElementsHidden={!isVisible}
importantForAccessibility={isVisible ? "yes" : "no-hide-descendants"}

After we standardised on react-native-reanimated-carousel, this was handled by the library, but it’s the kind of issue that only surfaces when you actually test with a screen reader.

2. Font Scaling

Users with visual impairments often increase their device’s font size. iOS and Android both support system-level font scaling, and React Native respects it by default: <Text> components scale with the system setting. But “respects it” and “works well with it” are different things.

At high font scales (1.7x and above), layouts break. Text overflows containers, buttons become too small relative to the text, and fixed-height elements clip their content. We built a useLargeText hook to detect when the user has large text enabled and adapt layouts accordingly:

const FONT_SCALE_THRESHOLD = 1.7;
export const MAX_CAPPED_FONT_SCALE = 2.143;

export const useLargeText = (options?: { threshold?: number }): boolean => {
  const { fontScale } = useWindowDimensions();
  return useMemo(
    () => fontScale >= (options?.threshold ?? FONT_SCALE_THRESHOLD),
    [fontScale, options?.threshold]
  );
};

The MAX_CAPPED_FONT_SCALE of 2.143 aligns with iOS accessibility settings; we found that setting it to exactly 2.0 caused clashing with custom line heights. This was one of those issues that only appeared on a real device with accessibility settings enabled; it was invisible in the simulator at default font scale.

Where we use this:

3. Navigation and Focus

When a screen transition happens, the screen reader’s focus should move to the new content. When a modal opens, focus should be trapped inside it. When an error appears, the screen reader should announce it. React Native’s accessibility APIs handle some of this automatically with navigation libraries, but custom transitions and modals need explicit focus management.

We added accessibilityLabel to navigation elements and ensured that error states were announced:

<View
  accessible
  accessibilityRole="alert"
  accessibilityLabel={`Error: ${errorMessage}`}
>
  <ErrorMessage>{errorMessage}</ErrorMessage>
</View>

How to Test

Testing accessibility is where most teams fall short, not because they don’t care, but because they don’t know what tools exist. Here’s what we used.

Accessibility Inspector (Xcode)

This is the most underused tool in the iOS ecosystem. Open Xcode, go to Xcode > Open Developer Tool > Accessibility Inspector. It shows you the accessibility tree of your running app: every element, its label, role, traits, and frame.

You can point it at the iOS simulator and inspect any element. It tells you:

I used this as a first pass on every screen. It’s faster than running VoiceOver and catches most structural issues. If an element shows up in the Inspector without a meaningful label, it needs fixing before you even start testing with a screen reader.

VoiceOver (iOS)

The real test. Turn on VoiceOver (Settings > Accessibility > VoiceOver on a device, or Cmd+F5 in the simulator) and navigate your app without looking at the screen.

Things that Accessibility Inspector can’t catch but VoiceOver reveals:

I made it a habit to VoiceOver-test any screen I worked on before marking it as done. It adds 5-10 minutes per screen and catches issues that no automated tool will find.

TalkBack (Android)

The Android equivalent. The UX is different from VoiceOver; TalkBack uses different gestures and announces elements slightly differently, so testing on both platforms matters. The most common issue I found was that accessibilityLabel values that worked well for VoiceOver sounded awkward or redundant with TalkBack’s default announcements.

BrowserStack Accessibility Testing

For testing on devices you don’t have physically, BrowserStack provides real device access with accessibility tools. This was particularly useful for testing on older Android devices where TalkBack behaviour differs from the latest version, and for verifying that font scaling worked across different screen sizes and densities.

BrowserStack also provides automated accessibility audits that flag common issues: missing labels, insufficient contrast, touch targets that are too small. It’s not a replacement for manual testing, but it catches the low-hanging fruit.

Manual Device Testing

There’s no substitute for testing on a real device with actual accessibility settings enabled. Some issues only manifest on physical hardware:

We kept a test device with large text enabled and VoiceOver active specifically for accessibility testing.

Common Issues and Fixes

A few patterns that came up repeatedly:

Touch Targets Too Small

Apple’s Human Interface Guidelines recommend a minimum touch target of 44x44 points. At default font scale, this is usually fine. At large font scales, the text grows but the touch target often doesn’t. We ensured minimum hitSlop or minHeight/minWidth on interactive elements.

Flat Lists and Accessibility

FlatList and ScrollView create interesting challenges for screen readers. Items that scroll off-screen are still in the accessibility tree by default. For long lists, this means the screen reader announces dozens or hundreds of items. We used accessibilityElementsHidden on items that were far from the visible area.

Images Without Alt Text

Every <Image> component should have either an accessibilityLabel (if it conveys information) or accessible={false} (if it’s decorative). This is the most common accessibility violation, and the easiest to fix.

Form Error Announcements

When a form validation error appears, sighted users see a red message below the field. Screen reader users see nothing unless you explicitly announce the error. We used accessibilityRole="alert" on error containers, which triggers an automatic announcement when the error appears.

Making It Stick

The hardest part of accessibility isn’t the implementation; it’s making it a sustained practice rather than a one-time effort. A few things helped:

That last point was a lesson I learned the hard way on this project. The app’s custom header component reimplemented navigation accessibility that the native Stack header provides for free. The more you lean on the platform, the less accessibility work you have to do yourself.

Conclusion

Accessibility isn’t a separate feature. It’s a quality of the features you’re already building. Every screen you ship either works for everyone or it doesn’t, and “everyone” includes people who can’t see the screen, people who need larger text, and people who navigate by voice.

The tools exist. Accessibility Inspector takes 30 seconds to open. VoiceOver is a toggle. The investment is 5-10 minutes per screen, and the payoff is an app that works for your entire audience rather than just the subset that happens to have good eyesight and steady hands.

For a retirement planning app, this wasn’t optional. But honestly, it shouldn’t be optional for any app.


Note: This post focuses on React Native with Expo, but the principles (screen reader testing, font scaling, focus management) apply to any mobile framework. The tools change; the approach doesn’t.



Previous Post
Brighton to Hastings on Foot
Next Post
Asset Format Strategy for React Native Apps