Accessibility

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Modals, Alerts, and Progress covered the visual side of surfacing state to the user — a toast, a dialog, a progress bar. This page covers the other half: making Places's UI usable with assistive technology, chiefly VoiceOver on iOS and TalkBack on Android, so that a screen reader user can navigate the same place list, place card, and forms as everyone else.

Accessibility Props: Grouping and Focus Order

  • accessible (boolean) marks a whole subtree as a single accessible element, hiding its individual children from the accessibility tree. This is the key grouping primitive: without it, a screen reader swiping across PlaceCard stops three separate times — once on the photo, once on the name/category text, once on the favourite star — instead of once on the card as a whole.

  • accessibilityLabel is the string a screen reader announces instead of trying to derive one from visible text. This is essential for icon-only controls — the favourite star toggle’s visible content, ★/☆, is not meaningful read aloud, so it needs an explicit label a sighted reading of the glyph doesn’t provide.

  • accessibilityRole says what kind of control this is — 'button', 'link', 'image', 'header', 'switch', 'text', and more — which lets a screen reader announce "button" (or "link", or "header") and enables the platform gestures and expectations that go with that role.

  • accessibilityState — { disabled, selected, checked, busy, expanded } — is the current state a screen reader should announce alongside the role, e.g. { selected: place.favourite } on the favourite toggle so VoiceOver says "star, button, selected".

  • accessibilityValue — { min, max, now, text } — is for a control with a continuous or textual value, such as the progress bar from Modals, Alerts, and Progress or a slider.

  • accessibilityHint is an optional, longer supplementary string describing the result of interacting with the control — for example "Removes this place from favourites" — read after the label, role, and state. Use it only when it adds information the label doesn’t already convey: over-using accessibilityHint makes VoiceOver verbose and annoying to listen to.

  • accessibilityActions / onAccessibilityAction declare custom actions a screen reader user can invoke beyond the default activation. An action descriptor is a plain { name, label } object; onAccessibilityAction receives an event whose nativeEvent.actionName identifies which action fired, typically dispatched from a switch. This is what lets a "Toggle favourite" action live directly on PlaceCard itself, so a screen reader user doesn’t have to navigate to the nested star Pressable separately — shown in full below.

  • accessibilityLiveRegion (Android) and its cross-platform alias aria-live — 'none' | 'polite' | 'assertive' — announce a live-updating region’s content changes automatically, e.g. a sync-status text updating in place, without the user needing to navigate back to it.

  • importantForAccessibility — 'auto' | 'yes' | 'no' | 'no-hide-descendants' — removes a purely decorative element, such as a background pattern View, from the accessibility tree entirely.

  • aria- aliases. React Native also accepts aria-label, aria-hidden, aria-selected, aria-disabled, aria-busy, aria-checked, aria-valuemin/aria-valuemax/aria-valuenow/aria-valuetext, aria-live, and role as web-familiar aliases for the accessibility props above. The two forms are interchangeable — React Native normalizes both to the same underlying platform semantics — so a team with React web experience may reach for the aria-* spelling instead, with no behavioral difference.

Grouping and Focus Order

accessible={true} groups a subtree into one stop, as above; the order elements are focused in by a screen reader’s swipe navigation normally follows visual/DOM order. When a custom layout needs a specific reading order — absolutely-positioned overlays, for instance, that need to be reachable in a sensible sequence rather than the order they happen to be drawn in — the levers available are accessibilityViewIsModal (iOS, scoping focus to a modal subtree) and deliberately ordering elements in JSX so the default swipe order already matches the intended one.

AccessibilityInfo, Large Text, Contrast, and Testing

AccessibilityInfo

  • AccessibilityInfo.isScreenReaderEnabled() returns a Promise<boolean>, and the 'screenReaderChanged' event (subscribed to with AccessibilityInfo.addEventListener) fires when that changes — together they let Places detect whether VoiceOver/TalkBack is running and branch behavior accordingly, e.g. skipping a purely decorative auto-playing animation.

  • AccessibilityInfo.isReduceMotionEnabled() and the 'reduceMotionChanged' event detect the OS-level "reduce motion" accessibility setting; Animations covers actually disabling or simplifying an animation — `PlaceCard’s fade-in, for example — once this setting is on.

  • AccessibilityInfo.announceForAccessibility(message) makes a screen reader announce an arbitrary string immediately, for state changes that don’t correspond to a focused element — "Place saved" after a successful add, for instance, complementing the toast from Modals, Alerts, and Progress with something a screen reader user can actually perceive, since a purely visual toast is otherwise easy to miss entirely.

Large Text

allowFontScaling defaults to true on Text, respecting the device’s OS-level text-size setting. maxFontSizeMultiplier caps how far a layout is willing to scale before truncation or wrapping breaks, which is useful on a tightly-constrained label — a badge, say — without disabling scaling altogether. The general rule: prefer flexible layouts (flexWrap, minHeight instead of a fixed height) that tolerate a much taller Text under a large accessibility font size setting, over numberOfLines truncation, wherever the truncated content genuinely matters to the user.

Colour Contrast and Dark Mode

styling.adoc’s `useColorScheme()/ThemeProvider dark-mode palette is also an accessibility concern: Places's own lightColors and darkColors token pairs should each independently meet at least WCAG AA contrast (4.5:1 for normal text) between text/textMuted and their background/surface counterparts. See Web Accessibility for the WCAG standard itself, its conformance levels, and contrast-checking tools — this page does not re-derive WCAG from scratch.

Testing

  • VoiceOver — iOS Settings > Accessibility > VoiceOver, or the Simulator’s Accessibility Inspector — and TalkBack — Android Settings > Accessibility > TalkBack — enabled manually on Places screens are the ground truth: no automated check replaces actually swiping through a screen with a screen reader on.

  • Xcode’s Accessibility Inspector (Xcode > Open Developer Tool > Accessibility Inspector) audits a running iOS build’s accessibility tree directly and includes a built-in audit that flags missing labels and low contrast.

  • Android Studio’s Layout Inspector and the standalone Accessibility Scanner app are the Android-side equivalents, surfacing the accessibility tree and flagging common issues on a running build.

Places: An Accessible PlaceCard

(a) Grouping the Card into One Accessible Element

accessible={true} on the outer Pressable groups the photo, name, category, and star into a single swipe stop, with a composed accessibilityLabel standing in for all of it:

// src/features/places/components/PlaceCard.tsx (excerpt -- accessible grouping added to the outer Pressable)
<Pressable
  style={({ pressed }) => [styles.card, pressed && styles.cardPressed]}
  onPress={() => onPress(place.id)}
  accessible={true}
  accessibilityRole="button"
  accessibilityLabel={`${place.name}, ${place.category}${place.favourite ? ', favourite' : ''}`}
>
  {/* photo, name, category, favourite toggle unchanged -- no longer individually announced */}
</Pressable>

(b) The Favourite Toggle as an Independent Control, Plus a Custom Action on the Card

The nested star Pressable gets its own role, label, and state, since accessible={true} on the card above would otherwise swallow it into the card’s single announcement. accessibilityActions/onAccessibilityAction on the outer card additionally exposes the same toggle as a custom action, so a screen reader user can trigger it without navigating to the nested star at all:

// src/features/places/components/PlaceCard.tsx (excerpt -- full component with accessible favourite toggle and a custom action)
export function PlaceCard({ place, onPress, onToggleFavourite }: PlaceCardProps) {
  const theme = useTheme();
  const styles = useMemo(() => createStyles(theme), [theme]);

  return (
    <Pressable
      style={({ pressed }) => [styles.card, pressed && styles.cardPressed]}
      onPress={() => onPress(place.id)}
      accessible={true}
      accessibilityRole="button"
      accessibilityLabel={`${place.name}, ${place.category}${place.favourite ? ', favourite' : ''}`}
      accessibilityActions={[{ name: 'toggleFavourite', label: 'Toggle favourite' }]}
      onAccessibilityAction={(event) => {
        switch (event.nativeEvent.actionName) {
          case 'toggleFavourite':
            onToggleFavourite(place.id);
            break;
        }
      }}
    >
      {place.photoUri !== null && (
        <Image source={{ uri: place.photoUri }} style={styles.photo} resizeMode="cover" />
      )}
      <View style={styles.body}>
        <Text style={styles.name}>{place.name}</Text>
        <Text style={styles.category}>{place.category}</Text>
      </View>
      <Pressable
        onPress={() => onToggleFavourite(place.id)}
        hitSlop={8}
        accessibilityRole="button"
        accessibilityLabel="Toggle favourite"
        accessibilityState={{ selected: place.favourite }}
      >
        <Text style={[styles.favourite, place.favourite && styles.favouriteActive]}>
          {place.favourite ? '★' : '☆'}
        </Text>
      </Pressable>
    </Pressable>
  );
}
Diagram of PlaceCard’s accessibility tree: the whole card grouped by accessible={true} with a composed accessibilityLabel and accessibilityRole of button

Because the outer Pressable’s own `onPress still fires onPress(place.id) as before, accessible={true} and the added props change only how the card is announced and what actions are available to a screen reader user — nothing about the card’s visual behavior or its existing onPress/onToggleFavourite contract changes for a sighted, touch-driven user.