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.
- Hinge Health Learning Center
- Articles
- Accessible React Native TextInput
Our Hinge Health Experts
Kate Dameron
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 theView
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
Credits
Cover photo by Gilles Lambert on Unsplash