Accessible by Default

What a screen reader announces, and the four props that control it: accessible, accessibilityLabel, accessibilityRole, accessibilityHint.

Syntax<Pressable accessibilityRole="button" accessibilityLabel="Save" accessibilityHint="Adds this to your list" />

A screen reader (VoiceOver on iOS, TalkBack on Android) moves focus one element at a time and speaks what it finds. On the web a <button> announces itself as a button because HTML carries that meaning. React Native elements carry none. A Pressable wrapping a Text is announced as the bare words inside it unless you say what it is.

The four props

accessible marks a view as a single element for assistive technology. Touchable components such as Pressable are accessible by default; a plain View or Image is not. accessibilityLabel is the text spoken instead of the children; with no label, the label is built by joining all child Text nodes with spaces. accessibilityRole says what kind of control it is ("button", "link", "header", "image", "checkbox", "switch" and others) and the reader appends that word. accessibilityHint describes what acting on it will do, and is read last, after a pause.

Shorthand and longhand

Since React Native 0.71 the same props also accept web names: role for accessibilityRole, aria-label for accessibilityLabel, aria-hidden for hiding a subtree. They are the same props, and role takes precedence when both are set. The value spellings differ slightly: role="heading" and role="img" where accessibilityRole uses "header" and "image".

What trips people up

An icon-only control has no child text to fall back on, so with no label it is announced as nothing at all. And a label replaces the children completely, so a stale label outlives the visible text.

Example

Example · tsx
// On screen: a row with the title on the left and a Save button on the
// right. The button text flips between "Save" and "Saved" on tap, and
// VoiceOver announces "Save Mountain Route to favourites, button".
import { useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";

type FavouriteRowProps = {
  title: string;
};

export default function FavouriteRow({ title }: FavouriteRowProps) {
  const [saved, setSaved] = useState(false);

  return (
    <View style={styles.row}>
      <Text style={styles.title}>{title}</Text>
      <Pressable
        // Pressable is already accessible; the role adds the word "button".
        accessibilityRole="button"
        // Without this the reader would only say "Save", which does not
        // say what gets saved.
        accessibilityLabel={
          saved ? `Remove ${title} from favourites` : `Save ${title} to favourites`
        }
        accessibilityHint="Changes your saved list"
        onPress={() => setSaved((wasSaved) => !wasSaved)}
        style={styles.button}
      >
        <Text style={styles.buttonText}>{saved ? "Saved" : "Save"}</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  // flexDirection defaults to "column", so a row has to ask for it.
  row: { flexDirection: "row", alignItems: "center", padding: 16, gap: 12 },
  title: { flex: 1, fontSize: 16 },
  button: { minHeight: 44, justifyContent: "center", paddingHorizontal: 16 },
  buttonText: { fontSize: 16, fontWeight: "600" },
});

When to use it

  • Make an icon-only toolbar usable with VoiceOver turned on.
  • Stop a screen reader announcing a tappable card as a flat run of words.
  • Give a form field the label a sighted user reads above it.

More examples

An icon-only button

A 44x44 tap area with a multiplication sign inside it. With no accessibilityLabel, TalkBack and VoiceOver would read the glyph itself or say nothing useful. Reach for this on every control whose only content is an icon, a glyph or an image: close buttons, back chevrons, overflow menus.

Example · tsx
import { Pressable, StyleSheet, Text } from "react-native";

type CloseButtonProps = {
  onClose: () => void;
};

export default function CloseButton({ onClose }: CloseButtonProps) {
  return (
    <Pressable
      accessibilityRole="button"
      // The glyph means nothing to a screen reader, so the label carries
      // the entire meaning of the control.
      accessibilityLabel="Close"
      onPress={onClose}
      style={styles.button}
    >
      <Text style={styles.glyph}>×</Text>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  button: { width: 44, height: 44, alignItems: "center", justifyContent: "center" },
  glyph: { fontSize: 24, lineHeight: 24 },
});

The same props, both spellings

Two headings and two card logos that behave the same way, written both ways. Reading someone else's code you will meet both forms, so both have to be recognisable. Pick one per codebase; mixing them in a single element is where the precedence rules start to matter.

Example · tsx
import { Image, StyleSheet, Text, View } from "react-native";

export default function PaymentHeader() {
  return (
    <View style={styles.block}>
      {/* Longhand: the accessibility* props. */}
      <Text accessibilityRole="header" style={styles.heading}>
        Payment
      </Text>
      {/* Shorthand: identical behaviour under the web name. Note the
          value is "heading" here and "header" above. */}
      <Text role="heading" style={styles.heading}>
        Payment
      </Text>

      {/* Image is not accessible by default, so it needs accessible. */}
      <Image
        accessible={true}
        accessibilityRole="image"
        accessibilityLabel="Visa"
        source={{ uri: "https://example.com/visa.png" }}
        style={styles.icon}
      />
      {/* The shorthand pair for the two props above. */}
      <Image
        accessible={true}
        role="img"
        aria-label="Visa"
        source={{ uri: "https://example.com/visa.png" }}
        style={styles.icon}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  block: { padding: 16, gap: 8 },
  heading: { fontSize: 20, fontWeight: "700" },
  icon: { width: 40, height: 24 },
});

Labelling a text input

A label above an input, with the two joined for assistive technology. On a web page the markup does this for you; here the visual association is purely positional, so a reader focused on the input hears nothing about what it wants. Use this on every field in every form.

Example · tsx
import { StyleSheet, Text, TextInput, View } from "react-native";

export default function EmailField() {
  return (
    <View style={styles.field}>
      {/* The visible label is a separate Text node. Nothing connects it
          to the input the way a web <label for> would. */}
      <Text nativeID="emailLabel" style={styles.label}>
        Email address
      </Text>
      <TextInput
        // Read on both platforms.
        accessibilityLabel="Email address"
        // Android only: points at the Text above by its nativeID.
        accessibilityLabelledBy="emailLabel"
        accessibilityHint="Used for your receipt"
        inputMode="email"
        autoCapitalize="none"
        style={styles.input}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  field: { padding: 16, gap: 6 },
  label: { fontSize: 14, color: "#444" },
  input: { borderWidth: 1, borderColor: "#999", borderRadius: 6, minHeight: 44, paddingHorizontal: 12 },
});

Discussion

  • Be the first to comment on this lesson.