In this tutorial, you’re going to learn how to make React Native apps more accessible. Specifically, we’re going to cover the following:
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.
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.
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:
Accessibility means designing your apps with consideration for all abilities, ensuring a positive and inclusive user experience for everyone.
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:
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
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:
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.
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.
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.
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.
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:
Once that’s done, the app’s design should be pretty accessible.
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.
In this section, we’ll take a look at four tools you can use to test the accessibility of your React Native 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 Xcode → Open Developer Tool → Accessibility Inspector.
From there, you can select the running iOS simulator instance:
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:
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 Settings → Accessibility → Accessibility Scanner and enable it.
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:
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:
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.
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 Settings → Accessibility → VoiceOver. 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.
To test in Android, run the app on your Android device. Once the app is running, go to Settings → Language and set it to your preferred language.
Next, go to Accessibility → Text-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 Accessibility → TalkBack then enable the setting**.** Once enabled, switch to the app and verify if the labels are read correctly as you tap.
Here are some resources to learn more about accessibility:
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.