mediumExtra#94

React Native accessibility

Prompt

Add accessibility props to a button component: accessible, accessibilityLabel, accessibilityRole, and accessibilityState — and make sure the announced state and the actual touch behavior stay in sync.

Solution

export default function AccessibleButton({ label, onPress, disabled }) {
  return (
    <TouchableOpacity
      accessible
      accessibilityLabel={label}
      accessibilityRole="button"
      accessibilityState={{ disabled }}
      disabled={disabled}
      onPress={onPress}
    >
      <Text>{label}</Text>
    </TouchableOpacity>
  )
}
Mentor's take

For a VoiceOver or TalkBack user, an unlabeled touchable is a silent rectangle — these four props are what turn it back into a button. Each does one job: accessible merges the touchable and its children into a single focusable element, so the screen reader lands on one thing instead of stepping into the inner Text separately. accessibilityLabel is the text that gets read. accessibilityRole="button" makes the reader announce "button," telling the user it's actionable. accessibilityState={{ disabled }} announces the disabled state — and here is the trap the question hides: it only announces. The separate disabled prop is what actually blocks touches. Pass one without the other and the screen reader either promises an interaction the app refuses, or silently ignores a live button.

Trade-off worth defending: for text buttons the explicit label looks redundant (readers fall back to child text), but icon-only buttons ship completely silent without one — so the maintainable team rule is "every interactive element gets a label," enforced by lint (eslint-plugin-react-native-a11y), not by memory. Keep testID (automation) and accessibilityLabel (humans) separate — hijacking labels as E2E selectors corrupts the spoken experience.

Red flag: "we'll add accessibility at the end." It retrofits badly, and claiming it's tested without ever turning on VoiceOver/TalkBack tells the interviewer no one has actually listened to the app.

Say it: "accessible merges the touchable into one focusable element, the label is what's read, the role announces the trait, and accessibilityState announces disabled — while the disabled prop is what actually blocks the tap, so the two must stay in sync."