Making accessible React Native apps

Introduction

In this tutorial, you’re going to learn how to make React Native apps more accessible. Specifically, we’re going to cover the following:

  • What is accessibility?
  • Designing apps with accessibility in mind
  • Accessibility in React Native apps
  • Accessibility testing tools

Of course, we cannot hope to cover everything about accessibility. It’s a pretty big subject and it’s a continuous journey. There’s always something that you can improve in order to make the experience just a little bit more pleasant for a certain user. Instead, what we hope to achieve in this tutorial, is to take that first step into making more accessible apps.

You can view the code used in this tutorial on its GitHub repo. The starter branch contains the not so accessible version of the app, while the a11y branch contains the more accessible version.

Prerequisites

To follow this tutorial, you need to know the basics of creating a React Native app.
The React Native development environment should also be set up on your machine.

We will be using React Native version 0.56 in this tutorial. We’ll also be using Yarn to install packages.

What is accessibility?

Before we proceed, it’s important that we all agree on what accessibility is, in the context of a mobile app. Accessibility or a11y, means making your apps usable to all users. Any person may have one or more form of disability. That usually includes but not limited to the following:

  • Vision differences - Examples include low vision, color vision differences, and total absence of sight. 
  • Mobility differences - Such as conditions affecting movement or dexterity. 
  • Neurodiverse conditions - Including autism spectrum conditions like Asperger's and other forms of autism. 
  • Hearing differences - Ranging from deafness to partial hearing differences. 
  • Learning differences - Such as dyslexia.

Accessibility means designing your apps with consideration for all abilities, ensuring a positive and inclusive user experience for everyone.

What you’ll be building

We won’t actually be building anything from scratch. Instead, we’re going to make a pre-built app more accessible. Here’s what the starter app looks like:

accessible-rn-starter

This won’t be how the final output will look like because we’ll also be taking design into consideration (though, only a little because I’m not really a designer).

If you want to follow along, clone the repo, switch to the starter branch and install the dependencies:

1git clone https://github.com/anchetaWern/RNa11y.git
2    cd RNa11y
3    git checkout starter
4    yarn install
5    react-native upgrade
6    react-native link
7    react-native run-android
8    react-native run-ios

Designing apps with accessibility in mind

In this section, we’ll redesign the app so that it becomes more accessible. We will be using the dos and don'ts on designing for accessibility from the GOV.UK website as a guide. Specifically, we’re going to adopt the following dos from their guide:

  • Use simple colors
  • Make buttons descriptive
  • Build simple and consistent layouts
  • Follow a linear, logical layout
  • Write descriptive links and heading
  • Use good contrasts and a readable font size
  • Use a combination of color, shapes, and text
  • Make large clickable actions

Right off the bat, you can see that the starter app violates some of these rules. The app is already following a few, but we can still improve on it.

Use simple colors

The starter app violates this rule because it’s using a dark color for its background. It’s not really easy on the eyes, so we need to update the app and card background:

1// file: App.js
2    const styles = {
3      container: {
4        flex: 10,
5        backgroundColor: "#FFF" // update this
6      }
7    };
1// src/components/Card.js
2    const styles = StyleSheet.create({
3      card: {
4        width: 120,
5        height: 140,
6        backgroundColor: "#3e3e3e", // update this
7      }
8    });

Also, update the Header component to match. This is because the items in the status bar aren’t really very readable when using a dark background:

1// src/components/Header.js
2    const styles = StyleSheet.create({
3      header: {
4        paddingTop: 10,
5        backgroundColor: "#ccc" // update this
6      },
7      header_text: {
8        fontWeight: "bold",
9        color: "#333", // update this
10      }
11    });

Once that’s done, the content should now be more readable.

Enlarge interactive elements

Next, we should increase the size of the buttons. This adjustment benefits individuals with mobility differences, as they might find it challenging to interact with smaller buttons.

If you inspect the app right now, you’ll see that there’s not much space we can work with. So even if we make the buttons larger, it will still be difficult to target a specific one because there won’t be ample whitespace between them. Though we still have some free space between each card so we’ll make use of that instead.

In your Card component, include the Dimensions module so that we can get the device’s width. We’ll use it to determine how much width each card can use. In this case, we have two cards in each row so we’ll just divide it by two and add a padding. We’re also making the height bigger because we’re anticipating the buttons to become bigger:

1// src/components/Card.js
2    
3    import { View, Text, Image, StyleSheet, Dimensions } from "react-native"; // add Dimensions
4    
5    const { width } = Dimensions.get("window");
6    
7    const cardPadding = 20;
8    const styles = StyleSheet.create({
9      card: {
10        width: (width / 2) - cardPadding, // update this
11        height: 150, // update this
12      }
13    });

Next, we can now proceed with updating the size and padding of the button:

1// src/components/IconButton.js:
2    
3    const icon_color = "#586069";
4    const icon_size = 25; // update this
5    
6    const styles = StyleSheet.create({
7      icon: {
8        // update these:
9        paddingLeft: 10, 
10        paddingRight: 10
11      }
12    });

At this point, each button should be enlarged and visible enough to click on.

Make buttons descriptive

Unfortunately, this isn’t really something that can be implemented all the time because of design constraints. If you check the app now, you’ll see that there’s not really enough space to accommodate labels for each button.

There is a solution, but we will end up giving up the current layout (two cards per row) for a one card per row layout. So the only feasible solution is to have a walkthrough for new users. This way, you can teach what each button is used for. I won’t really be covering how to do that, but there’s a good component which allows you to implement it easily.

Prioritize clear contrast and legible font sizes

I believe the app currently offers clear contrast. However, to ensure optimal readability for every user, we'll make a few more adjustments.

First, we have to differentiate between each individual card and the app’s background. We can do that by applying a darker background color:

1// src/components/Card.js
2    const cardPadding = 20;
3    const styles = StyleSheet.create({
4      card: {
5        width: width / 2 - cardPadding,
6        height: 150,
7        backgroundColor: "#e0e0e0", // update this
8      }
9    });

Next, we need to differentiate between the card’s body and its contents:

1// src/components/Card.js
2    const styles = StyleSheet.create({
3      name: {
4        fontSize: 16,
5        color: "#3a3f46", // update this
6      }
7    });
1// src/components/IconButton.js
2    
3    const icon_color = "#3a3f46"; // update this
4    const icon_size = 25;

Lastly, we need to make enlarge the text. While there’s no general agreement as to what font size should we be using to optimize accessibility, a few people seem to swear by 16px so we’re also going with that:

1const styles = StyleSheet.create({
2      name: {
3        fontSize: 16, // update this
4      }
5    });

We’ve skipped the following because we’re already following them:

  • Write descriptive links and heading
  • Follow a linear, logical layout
  • Use a combination of color, shapes, and text
  • Build simple and consistent layouts

Once that’s done, the app’s design should be pretty accessible.

Accessibility in React Native apps

The prior section primarily addressed the visual aspects of accessibility. In this segment, we'll explore ways to optimize the app for screen reader users.

For context, a screen reader vocalizes the content that users are currently interacting with on the screen. This tool is often utilized by individuals with vision differences. When a screen reader is active, users typically need to double-tap to initiate the desired action.

In order for a screen reader to be useful, we need to properly label all the relevant components that a user will most likely interact upon. In React Native, this can be done by adding accessibility props. Here’s an example of how we can add these props:

1// src/components/Header.js
2    const Header = ({ title }) => {
3      return (
4        <View
5          style={styles.header}
6          accessible={true}
7          accessibilityLabel={"Main app header"}
8          accessibilityRole={"header"}
9        >
10          <Text style={styles.header_text}>{title}</Text>
11        </View>
12      );
13    };

Let’s go through each of the accessibility props we’ve added to the Header component:

  • accessible - accepts a boolean value that’s used to mark whether a specific component is an accessible element or not. This means that the screen reader will read whatever label you put on it. Be careful with using this though, as it makes all of its children inaccessible. In the Header component above, this makes the Text component inside the View inaccessible. So the screen reader won’t actually read the title indicated in the header. It will only read the accessibilityLabel you’ve passed to the View instead. It’s a good practice to only set the accessible prop to true if you know that the component doesn’t have any child that’s supposed to be treated as an accessible element.
  • accessibilityLabel - the text you want the screen reader to read when the user touches over it. A good practice when using this prop is to be as descriptive as possible. Remember that the user will only rely on what’s being read by the screen reader. They actually have no idea of the context a specific component is in, so it’s always useful to repeat it in your labels. For example, each of the buttons in each card should still mention the name of the Pokemon.
  • accessibilityRole - the general role of the component in this app. Examples include: button, link, image, text, and in this case header. Note that header doesn’t only indicate the app’s main header. It can also indicate a section header or a list header.

The next component we’ll update is the IconButton because it’s important that the user knows that those buttons we’ve added are actually buttons:

````javascript
// src/components/IconButton.js
const IconButton = ({ icon, onPress, data, label }) => {
return (
<TouchableOpacity
accessible={true}
accessibilityLabel={label}
accessibilityTraits={"button"}
accessibilityComponentType={"button"}
onPress={() => {
onPress(data.name);
}}
>

);
};

1From the code above, you can see that we’re accepting a new `label` prop which we then use as the value for the `accessibilityLabel`. We’ve also set the component to be `accessible` which means that when the user’s finger goes over it, the screen reader will read out the `accessibilityLabel`. 
2
3But what about `accessibilityTraits` and `accessibilityComponentType`? Well, they are the old way of setting the `accessibilityRole`. `accessibilityTraits` is only for iOS and `accessibilityComponentType` is only for Android. As [mentioned in the docs](https://facebook.github.io/react-native/docs/accessibility#accessibilitytraits-ios), these props will be deprecated soon. We’re only using it because `TouchableOpacity` doesn’t seem to be accepting `accessibilityRole`. The trait (button) wouldn’t show up as I was testing with the accessibility inspector. We’ll go over this tool in the next section.
4
5Lastly, we update the `Card` component so it passes the correct labels to each of the IconButton. We’re also making the Pokemon Image and Text accessible:
6
7``` javascript
8    // src/components/Card.js
9    const Card = ({ item, viewAction, bookmarkAction, shareAction }) => {
10      return (
11        <View style={styles.card}>
12          <Image
13            source={item.pic}
14            style={styles.thumbnail}
15            accessible={true}
16            accessibilityRole={"image"}
17            accessibilityLabel={`${item.name} image`}
18          />
19          <Text style={styles.name} accessibilityRole={"text"}>
20            {item.name}
21          </Text>
22          <View style={styles.icons}>
23            <IconButton
24              icon="search"
25              onPress={viewAction}
26              data={item}
27              label={`View Pokemon ${item.name}`}
28            />
29            <IconButton
30              icon="bookmark"
31              onPress={bookmarkAction}
32              data={item}
33              label={`Bookmark Pokemon ${item.name}`}
34            />
35            <IconButton
36              icon="share"
37              onPress={shareAction}
38              data={item}
39              label={`Share Pokemon ${item.name}`}
40            />
41          </View>
42        </View>
43      );
44    };

In case you’re wondering why we didn’t add the accessible and accessibilityLabel prop in the Pokemon label, it’s because the Text component is accessible by default. This also means that the screen reader automatically reads the text inside of this component.

Accessibility testing tools

In this section, we’ll take a look at four tools you can use to test the accessibility of your React Native app.

Testing accessibility while developing the app

In iOS, you can use the Accessibility Inspector tool in Xcode. Because it’s in Xcode, you have to run the app from Xcode. You can do that by opening the RNa11y.xcodeproj or RNa11y.xcworkspace file inside your project’s ios directory. Then run the app using the big play button located on the upper left side of the screen.

Once the app is running, you can open the Accessibility Inspector tool by going to XcodeOpen Developer ToolAccessibility Inspector.

From there, you can select the running iOS simulator instance:

ios-accessibility-inspector-1

Once you’ve selected the simulator, click on the target icon right beside the drop-down. This activates the inspection mode. You can then hover over the components which we updated earlier and verify whether the inspector is reading the labels correctly:

ios-accessibility-inspector-2

For Android testing, you can use the Accessibility Scanner app. Unlike the Accessibility Inspector in iOS, you have to install it on your emulator or device in order to use it. Once installed, go to SettingsAccessibilityAccessibility Scanner and enable it.

android-accessibility-scanner

Once it’s enabled, switch to the app that we’re working on and click the floating blue button. This will scan the app for any accessibility issues. Once it’s done scanning, you can click on any of the indicated areas to view the suggestion:

image-contrast-warning

The easiest way to solve this issue is by making the card’s background color lighter. You can also try increasing the contrast of the image as suggested.

Interestingly, if you remove the accessibility props from the image and scan again, you’ll see that it will no longer complain about the contrast:

1// src/components/Card.js
2    const Card = ({ item, viewAction, bookmarkAction, shareAction }) => {
3      return (
4        <View style={styles.card}>
5          <Image
6            source={item.pic}
7            style={styles.thumbnail}
8          />
9          ...
10        </View>
11      );
12    };

This can mean that the scanner only gets picky when you’ve marked a component as accessible. To test this assumption, try removing the accessibility props from the IconButton:

1// src/components/IconButton.js
2    const IconButton = ({ icon, onPress, data, label }) => {
3      return (
4        <TouchableOpacity
5          onPress={() => {
6            onPress(data.name);
7          }}
8        >
9        ...
10        </TouchableOpacity>
11      );
12    };

If you run the scanner again, you’ll see that it actually picks up on the issue:

item-description-warning

Manual accessibility testing

As with anything, it’s always important to test things manually so you know the actual experience your users are getting. After all, accessibility is all about improving the user experience that your users get when using the app.

Testing in iOS

To test things manually in iOS, open Xcode and run the app on your iOS device. You can also do this from the simulator but that kinda beats the purpose of manual testing. You won’t really have an accurate “feel” of the experience if you’re just testing from a screen.

Once the app is running on your device, go to SettingsAccessibilityVoiceOver. From there, you can select the Speech menu to change the voice (I personally prefer Siri Female). You can also adjust the speaking rate. Adjust a little bit more from the mid-point should be fast enough for most people.

Once you’re done adjusting the settings, enable the VoiceOver setting then switch to the app. From there, you can tap on each of the accessibility areas that we’ve set to verify if it’s being read correctly.

Testing in Android

To test in Android, run the app on your Android device. Once the app is running, go to SettingsLanguage and set it to your preferred language.

Next, go to AccessibilityText-to-speech options and make sure the Default language status is fully supported. If not, you have to go to the language settings again and select a supported language.

The equivalent of VoiceOver in Android is TalkBack, you can enable it by going to AccessibilityTalkBack then enable the setting**.** Once enabled, switch to the app and verify if the labels are read correctly as you tap.

Further reading

Here are some resources to learn more about accessibility:

Conclusion

That wraps it up! In this guide, you've discovered how to enhance the accessibility of React Native apps for all users, regardless of their abilities. I hope you'll integrate these insights into your development process, ensuring every user experiences a seamless and inclusive interaction with your app.

You can view the code used in this tutorial on its GitHub repo.