Person using mobile device 2

Accessible React Native TextInput

At Hinge Health, it's very important to us to ensure that all of our users have the best experience possible while using our apps. This means making sure that we write code that is accessible for all users.

Published Date: Apr 21, 2021
Person using mobile device 2

Our Hinge Health Experts

Kate Dameron
Native Oklahoman, dog/cat mom, hiker, art maker, and enjoys baking the occasional healthy (or not) treat.

At Hinge Health, it's very important to us to ensure that all of our users have the best experience possible while using our apps. This means making sure that we write code that is accessible for all users.

We use React Native (RN) because it allows us to have one repo that is compatible with multiple OS platforms. My team and I found that, besides this documentation page with a list of available accessibility props, there are not a lot of additional resources on how to solve issues of accessibility holistically in RN. I am hoping to make things a bit less scarce by sharing some examples of how we've solved building accessible components.

This is the first in a series of posts that will feature fully accessible atomic components built in React Native.

Disclaimer: I do not claim to be an expert on accessibility nor React Native. My goal is to share the things I've learned through trial and error and through collaboration with other Engineers at Hinge Health. I expect, through this process, that I will discover new things I didn't know before.

NOTE: I'll be using React Native with hooks and, since this isn't a tutorial on either library, I will assume that the reader has some basic understanding.

So let's get started:

How to build an accessible text input in React Native

What are the accessibility requirements of a text input? As a user, I should be able to...

  • focus form inputs with screen reader specific gestures

  • understand which form input is focused via a visual label that persists even after I've started typing

  • hear the label for a focused input announced by a screen reader

  • know if a focused input is disabled, checked, or selected via a screen reader announcement

  • perceive colors (border, label, placeholder, background) that are at least 4.5.1 contrast ratio

With all of this in mind, let's build the thing.

In the example below I have a Text and a TextInput wrapped in a View to compose a form input for the user's email.

On the View there are three important props. These props are part of the RN accessibility API and they can be passed to a number of RN components.

  • accessible: groups the children of the View into a single focusable element. The text will be read altogether and action can be taken on the element within. The children will not be focusable by themselves.

NOTE: There should only be one interactive child element (button, form input, etc.) inside an accessible View but there can be any number of text elements

  • accessibilityLabel: this label will replace the text in the accessible component. In the example below, it's used to convey slightly more context for users.

  • accessibilityState: screen readers will announce the state of a component, "disabled", "checked", "selected", etc.

NOTE: This prop takes an object and it's best to define the object before passing it to the prop per the documentation

I've also used the useState hook to update the value and toggle the editable state.

import React, { useState } from 'react'; import { TextInput, View, Text } from 'react-native'; const AccessibleTextInput = () => { const value, setValue = useState('') const editable, setEditable = useState(true) const accessibilityState = { disabled: !editable } return ( <View accessible accessibilityLabel="Enter email" accessibilityState={accessibilityState} > <Text>Email</Text> <TextInput placeholder="example@domain.com" editable={editable} value={value} onChangeText={(text) => setValue(text)} /> </View> ) }; export default AccessibleTextInput;

Cool, that seems like a good start but we're missing a few things. Right now our input looks like this:

We need some styles to clearly show where the input is as well as the current state. Let's focus on three primary states for now, editable/unfocused, editable/focused, and disabled.

In the code below, I've chosen three different colors to represent these three states.

NOTE: A disabled element does not need to have a color contrast ratio of 4.5.1. However, the disabled state should either be announced by a screen reader or the element should be hidden from the screen reader altogether.

The editableTextInputColor with a value of #494949 and the focusedTextInputColor with a value of #0D12B9 pass a color contrast check against #FFF or white. I used the contrast checker from coolers.co but there are quite a few different ones available!

Additionally, I've added another useState to toggle the isFocused state of the element.

import React, { useState } from 'react'; import { TextInput, View, Text, StyleSheet } from 'react-native'; const editableTextInputColor = '#494949'; const disabledTextInputColor = '#BBB'; const focusedInputColor = '#0D12B9' const minimumTouchableSize = 48; const AccessibleTextInput = () => { const value, setValue = useState('') const editable, setEditable = useState(true) const isFocused, setFocus = useState(false) const textInputColor = editable ? editableTextInputColor : disabledTextInputColor; const styles = StyleSheet.create({ label: { color: isFocused ? focusedInputColor : textInputColor }, input: { backgroundColor: '#FFF', padding: 8, height: minimumTouchableSize, width: "100%", borderColor: isFocused ? focusedInputColor : textInputColor, borderWidth: isFocused ? 2 : 1, borderRadius: 4, marginTop: 8 } }); const accessibilityState = { disabled: !editable } return ( <View accessible accessibilityLabel="Enter email" accessibilityState={accessibilityState}> <Text style={styles.label}>"Email"</Text> <TextInput placeholder="example@domain.com" placeholderTextColor={textInputColor} value={value} onChangeText={(text) => setValue(text)} editable={editable} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)} /> </View> ) }; export default AccessibleTextInput;

Here's what our three states look like right now.

editable/unfocused

editable/focused

disabled

I've run into an issue though. When I run this on my iOS device I don't hear the screen reader announce the "disabled" state. I can utilize the accessibilityLabel to fix this issue. By importing Platform from react-native I can check the OS of the user's device and use the label to announce that the input is disabled.

import React, { useState } from 'react'; import { TextInput, View, Text, Platform } from 'react-native'; const isAndroid = Platform.os === 'android'; const AccessibleTextInput = () => { const value, setValue = useState('') const editable, setEditable = useState(true) const accessibilityState = { disabled: !editable } return ( <View accessible accessibilityLabel={ isAndroid ? accessibilityLabel : `Enter email ${!editable ? ': Disabled!' : ''}` } accessibilityState={accessibilityState}> <Text style={styles.label}>Email</Text> <TextInput placeholder="email@domain.com" value={value} onChangeText={(text) => setValue(text)} editable={editable} /> </View> ) }; export default AccessibleTextInput;

We can take this component a step further though! Right now, this input only works for an email text input. We can make it reusable so that it's just a matter of plug 'n play to ensure that all text inputs in our app, regardless of type, are accessible. We do this by passing in the label, accessibilityLabel, inputValue, and placeholderText as props.

import React, { useState } from 'react'; import { TextInput, View, Text, Platform, StyleSheet } from 'react-native'; const isAndroid = Platform.os === 'android'; const editableTextInputColor = '#494949'; const disabledTextInputColor = '#BBB'; const focusedInputColor = '#0D12B9' const minimumTouchableSize = 48; const AccessibleTextInput = ({ label = 'Email', inputValue = '', placeholderText = 'example@domain.com', accessibilityLabel = 'Enter email' }) => { const value, setValue = useState(inputValue) const editable, setEditable = useState(true) const isFocused, setFocus = useState(false) const textInputColor = editable ? editableTextInputColor : disabledTextInputColor; const styles = StyleSheet.create({ label: { color: isFocused ? focusedInputColor : textInputColor }, input: { backgroundColor: '#FFF', padding: 8, height: minimumTouchableSize, width: "100%", borderColor: isFocused ? focusedInputColor : textInputColor, borderWidth: isFocused ? 2 : 1, borderRadius: 4, marginTop: 8 } }); const accessibilityState = { disabled: !editable } return ( <View accessible accessibilityLabel={ isAndroid ? accessibilityLabel : `${accessibilityLabel}${!editable ? ': Disabled!' : ''}` } accessibilityState={accessibilityState}> <Text style={styles.label}>{label}</Text> <TextInput placeholder={placeholderText} placeholderTextColor={textInputColor} value={value} onChangeText={(text) => setValue(text)} editable={editable} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)} /> </View> ) }; export default AccessibleTextInput;

Great! Now we have a working accessible text input. Try it out yourself in this expo snack!

Resources

  • React Native accessibility - API

  • Color Contract Checker - Coolors

Credits