Forms are everywhere in mobile apps - authentication flows, data entry, support requests, onboarding... If your app has a login screen, a form is likely the first thing a new user interacts with. That makes accessibility here not just a nice-to-have, but a first impression.
The problem is that forms are consistently one of the most broken areas for assistive technology users. Missing labels, keyboard traps, silent validation errors, focus going nowhere after submission - these are issues that make an app unusable for a significant portion of your users.
This guide is a complete reference for building forms that work for everyone in React Native, whether users are navigating with their fingers, an external keyboard, a screen reader or voice input. Code examples throughout show both what to do and why.
A fully working demo repo is available to fork and test on a real device - check it out at rn-accessible-form-demo.
Labels
Every form field needs a label, whether a text input, checkbox or radio/submit buttons. No exceptions.
Don't rely on placeholders
Placeholder text disappears the moment a user starts typing. Screen readers will read it initially, but once it's gone, there's no way for them to recall what the field was for without clearing their input. Placeholders are useful as hints, not as labels.
Use a visual label + accessibilityLabel
To avoid screen readers announcing the same information twice (once for the visual label, once for the input), hide the visual label from assistive technology and put the full label on the input itself.
<Text
importantForAccessibility="no"
accessibilityElementsHidden
>
Email address*
</Text>
<TextInput
accessibilityLabel="Email address, required"
/>
importantForAccessibility="no" handles Android, and accessibilityElementsHidden handles iOS. Together they tell assistive technology to skip the visual label entirely - the accessibilityLabel on the TextInput is the single source of truth for screen readers.
Required fields
Declare required fields within the accessibilityLabel - not just with a visual asterisk. A screen reader user has no way of knowing what an asterisk means unless you tell them.
// ❌ Screen reader reads "Email address" — no indication it's required
<TextInput accessibilityLabel="Email address" />
// ✅ Screen reader reads "Email address, required"
<TextInput accessibilityLabel="Email address, required" />
When to use accessibilityHint
Use accessibilityHint for additional context that doesn't belong in the label - format guidance, what the field is used for or what happens after interaction. It's read after the label and role, and users can disable hints in their accessibility settings, so never put critical information here.
<TextInput
accessibilityLabel="Date of birth, required"
accessibilityHint="Enter in DD/MM/YYYY format"
/>
Headings
Use accessibilityRole="header" on form titles and section headings. Screen reader users can navigate by heading to jump quickly to sections, and it's read aloud as a heading rather than plain text.
<Text accessibilityRole="header">
Contact Us
</Text>
Radio Buttons and Predefined Options
When information can be selected from a known set of options, radio buttons are more accessible than a free text input. Users can see all available choices at a glance and select without typing, which reduces errors and cognitive load, particularly for users navigating by screen reader or keyboard.
Use accessibilityRole="radio" on each option and accessibilityRole="radiogroup" on the container. The group tells screen readers the options are related. accessibilityState={{ selected }} reflects the selected state so screen readers announce "selected" or "not selected" alongside the label.
<View
accessibilityRole="radiogroup"
accessibilityLabel="Preferred contact method, required"
>
<TouchableOpacity
accessibilityRole="radio"
accessibilityState={{ selected: selected === 'email' }}
accessibilityLabel="Email"
onPress={() => setSelected('email')}
>
{/* custom styled radio UI */}
</TouchableOpacity>
<TouchableOpacity
accessibilityRole="radio"
accessibilityState={{ selected: selected === 'phone' }}
accessibilityLabel="Phone"
onPress={() => setSelected('phone')}
>
{/* custom styled radio UI */}
</TouchableOpacity>
</View>
Error handling follows the same pattern as text inputs - embed the error in the group's accessibilityLabel and move focus to the group container. More on this below.
Checkboxes and Legal Consent
For any action with legal or financial implications - agreeing to terms and conditions, signing up to a subscription, confirming a payment - always present the option as an explicit checkbox. Users should make a deliberate, informed choice rather than having consent assumed.
Use accessibilityRole="checkbox" and accessibilityState={{ checked }}.
<TouchableOpacity
accessibilityRole="checkbox"
accessibilityState={{ checked }}
accessibilityLabel={
'I agree to the Terms and Conditions, required'
}
accessibilityHint="You must agree to the Terms and Conditions before submitting"
onPress={() => setChecked(v => !v)}
>
{/* custom styled checkbox UI */}
</TouchableOpacity>
If the checkbox is required and unchecked on submission, the error follows the same focus and label pattern as other fields as detailed below - move focus to the checkbox and embed the error in the accessibilityLabel.
Keyboard Behaviour
Users should be able to navigate through your entire form using the return key on their on-screen keyboard, without getting stuck.
Set the correct keyboard type
Match the keyboard to the expected input. This reduces cognitive load and prevents errors.
<TextInput
accessibilityLabel="Phone number"
keyboardType="phone-pad"
/>
Common values: default, email-address, numeric, phone-pad, decimal-pad. Full list in the React Native docs.
returnKeyType
Set returnKeyType="next" on every field except the last, and returnKeyType="done" on the final field. This tells users where they are in the form and what pressing return will do.
<TextInput
accessibilityLabel="Email address, required"
returnKeyType="next"
/>
<TextInput
accessibilityLabel="Phone number"
keyboardType="phone-pad"
returnKeyType="done"
/>
Chain focus between fields
When a user presses the return key, focus should move to the next field automatically. Use onSubmitEditing, submitBehaviour and refs to wire this up.
const phoneInputRef = useRef<TextInput>(null);
<TextInput
accessibilityLabel="Email address, required"
returnKeyType="next"
onSubmitEditing={() => phoneInputRef.current?.focus()}
/>
<TextInput
ref={phoneInputRef}
accessibilityLabel="Phone number"
keyboardType="phone-pad"
returnKeyType="done"
onSubmitEditing={submitForm}
submitBehavior='blurAndSubmit'
/>
When a user reaches the final field and presses 'Done' or their return key, onSubmitEditing fires submitForm so the form can be submitted entirely from the keyboard without needing to reach for a button.
⚠️ iOS gotcha: numeric keyboards and onSubmitEditing
If your final field uses keyboardType="phone-pad" on iOS, onSubmitEditing will not fire when the user taps 'Done'. This is an iOS platform limitation. Make sure your submit button is clearly reachable after dismissing the keyboard, and consider adding a toolbar above the keyboard with a 'Submit' action.
Validation Errors
This is where most forms fall apart. Getting validation right is the difference between a form that's usable by screen reader users and one that isn't.
Field-level errors
When validation fails, the first field with an error should receive focus automatically. The error message should be part of that field's accessibilityLabel so it's read out immediately - users shouldn't need to navigate to a separate error element to understand what went wrong.
For example:
const emailInputRef = useRef<TextInput>(null);
useEffect(() => {
if (showEmailError) {
emailInputRef.current?.focus();
}
}, [showEmailError]);
<TextInput
ref={emailInputRef}
accessibilityLabel={`Email address, required, ${showEmailError ? `, ${emailErrorText}` : ''}`}
returnKeyType="next"
onSubmitEditing={() => phoneInputRef.current?.focus()}
/>
When the error appears, focus moves to the email input and VoiceOver/TalkBack reads: "Email address, required, Please enter a valid email address" or your equivalent email error message.
The visual error message should still render on screen for sighted users, but hide it from assistive technology to avoid it being read twice:
{showEmailError && (
<Text importantForAccessibility="no" accessibilityElementsHidden>
{emailErrorText}
</Text>
)}
Multiple field errors
If multiple fields fail validation simultaneously, focus the first field with an error. Use a useEffect that watches all error states and prioritises accordingly. For example:
useEffect(() => {
if (showEmailError) {
emailInputRef.current?.focus();
} else if (showPhoneError) {
phoneInputRef.current?.focus();
}
}, [showEmailError, showPhoneError]);
Submission errors
For errors that aren't tied to a specific field, e.g. network failures, server errors or unexpected problems, use AccessibilityInfo.announceForAccessibility to read the message out immediately. This is a live announcement that interrupts the current screen reader output, so the user knows something went wrong without needing to navigate anywhere.
import { AccessibilityInfo } from 'react-native';
const submitForm = async () => {
try {
await sendFormData();
} catch {
setSubmissionError(true);
AccessibilityInfo.announceForAccessibility(
'Something went wrong. Please try again.'
);
}
};
Display the error visually above the submit button so sighted users can see it. Since announceForAccessibility already handles screen readers, you can leave it visible to all - there's no double-reading issue here.
{submissionError && (
<Text>{submissionErrorText}</Text>
)}
Success State and Focus Management
What happens after a form submits successfully is just as important as what happens during validation. If focus stays on the submit button or disappears entirely, screen reader users have no way of knowing the form worked.
Success screen
The cleanest approach is to replace the form entirely on success - no ambiguity about what happened. Render a success message and move focus to the heading automatically using AccessibilityInfo.setAccessibilityFocus.
const successRef = useRef<Text>(null);
useEffect(() => {
if (submitted) {
setTimeout(() => {
const reactTag = (successRef.current as any)?._nativeTag;
if (reactTag) {
AccessibilityInfo.setAccessibilityFocus(reactTag);
}
}, 100);
}
}, [submitted]);
The small delay gives the success screen time to render before focus is moved - without it, the element may not yet exist in the native view hierarchy.
accessible prop
For setAccessibilityFocus to work on a Text element, it must be marked as accessible. Without this, screen readers can't receive focus on it.
<Text
ref={successRef}
accessible
accessibilityRole="header"
>
Form submitted!
</Text>
When focus moves to this element, VoiceOver reads: "Form submitted!, heading."
Toasts and timed notifications
A common alternative to a full success screen is a toast - a brief notification that appears on screen and disappears after a few seconds. They're quick to implement and feel lightweight, but they come with real accessibility tradeoffs worth understanding before reaching for them.
The issue
A toast that appears and disappears visually gives sighted users a moment to read it. For screen reader users, that same toast may never be announced at all or announced mid-navigation, interrupt the wrong thing, or disappear before the user has finished processing it. Timed components that vanish without warning are a WCAG failure.
Making toasts accessible
The minimum requirement is that the content is announced to screen readers immediately when it appears, regardless of whether the user navigates to it. Use AccessibilityInfo.announceForAccessibility to read the message out loud at the moment the toast mounts - the same pattern used above for submission errors. For example:
useEffect(() => {
if (toastVisible) {
AccessibilityInfo.announceForAccessibility(toastMessage);
}
}, [toastVisible]);
This ensures screen reader users hear the message even if the toast disappears before they navigate to it.
Timing
If a toast auto-dismisses, the duration needs to be long enough for users to read it - WCAG recommends at least five seconds for short messages and longer for anything more complex. Where possible, give users a way to dismiss the toast manually rather than relying on a timer alone.
When to use
Toasts are appropriate for low-stakes, non-critical feedback, e.g. "Saved", "Copied to clipboard", "Message sent". They are not appropriate for errors, warnings, or anything the user needs to act on - these should use the focus-based patterns covered earlier in this guide.
For form submission success specifically, a full success screen with focus management is the more accessible choice. A toast is a reasonable fallback if the form remains visible after submission (for example, a search form or a filter), but the message must still be announced via announceForAccessibility - it cannot rely on the user navigating to it before it disappears.
Review, Undo and Reversible Actions
Before a form submission is final - particularly one with legal or financial implications - users should have an opportunity to review what they've entered and reverse the action if needed.
This matters for all users, but especially for screen reader users who navigate sequentially and may not have a complete picture of everything they've filled in. A confirmation step, a summary screen, or a clear cancellation path gives everyone the chance to catch mistakes before they matter.
Practical patterns to consider:
Confirmation step - for high-stakes actions (subscribing to a service, making a payment, deleting an account), show a summary of what the user is about to do and ask them to confirm before completing the action;
Cancellation - make it easy to cancel or go back at any point in a multi-step form and ensure the cancel action is reachable by keyboard and screen reader;
Destructive actions - if a form action is irreversible (closing an account, submitting a legal document), say so explicitly in the UI - not just in the terms - so users understand what they're agreeing to before they confirm;
Session persistence - if a user navigates away from a long form, consider persisting their input so they don't lose work. Losing form data is frustrating for any user; for someone navigating with assistive technology it can mean starting a lengthy process from scratch.
The success screen in the demo form demonstrates the lightest version of this - the user can tap "Submit another response" to go back and the form resets cleanly. For real subscription or payment flows, a dedicated review screen before final submission is the right pattern.
Voice input
If you've followed the patterns above, your form will work with voice input without any additional changes. The accessibilityLabel values are what voice control software (iOS Voice Control, Android Voice Access) uses to identify fields - clear, descriptive labels mean users can say "tap Email address" and the right field activates.
To access voice input:
iOS - Settings → Accessibility → Voice Control → enable. Tap the microphone icon in any text field to dictate
Android - Download Gboard, enable Google voice typing and tap the microphone icon in any input
Testing your form
Building accessible forms and testing accessible forms are two different things. Always verify with real assistive technology on a real device - automated tools won't catch focus management or announcement order issues.
VoiceOver (iOS)
- Settings → Accessibility → VoiceOver → enable
- Swipe right to navigate forward through elements, left to go back
- Double-tap to activate
- Navigate through the entire form: are all fields announced correctly? Are errors read out and fields focused as expected? Does focus move to the success heading on submission?
TalkBack (Android)
- Settings → Accessibility → TalkBack → enable
- Swipe right/left to navigate, double-tap to activate
- Check the same things as VoiceOver - but also verify that
importantForAccessibility="no"is working correctly, as Android and iOS handle this differently in some edge cases
External keyboard
- Connect a Bluetooth keyboard and navigate using Tab and Return only
- You should never get trapped in a field and focus should always be visible
Final Checklist
- Every field has an
accessibilityLabel - Required fields say "required" in the label
- Form title uses
accessibilityRole="header" - Predefined options use radio buttons rather than free text where appropriate
- Radio groups use
accessibilityRole="radiogroup"on the container - Each radio option uses
accessibilityRole="radio"andaccessibilityState={{ selected }} - Legal consent uses an explicit checkbox with
accessibilityRole="checkbox" - Checkbox state is declared explicitly in the accessibilityLabel as "checked" or "not checked"
-
returnKeyTypeis set correctly on all fields - Focus moves between fields via onSubmitEditing
- The iOS numeric keyboard gotcha is accounted for if relevant
- Validation errors auto-focus the first errored field
- Error text is included in the field's
accessibilityLabel - Visual error messages are hidden from assistive technology
- Submission errors use
announceForAccessibility - Success state moves focus to a confirmation heading
- Toasts announce their content via
announceForAccessibilitywhen they appear, if necessary to use, and are visible for long enough to read - Toasts are not used for errors or actions that require user attention
- High-stakes or irreversible actions have a confirmation or review step
- Users can cancel or go back at any point in a multi-step form
- Destructive or legal actions describe their consequences clearly in the UI
- Tested with VoiceOver / TalkBack or voice input on real devices, as well as keyboards
Accessible forms aren't significantly more work than inaccessible ones, but the patterns need to be deliberate from the start. Retrofitting accessibility into a form that wasn't designed for it is much harder than building it in correctly the first time.
The demo repo has a fully working implementation of everything in this guide - fork it, run it on a real device and break things deliberately to see what happens. rn-accessible-form-demo
If you found this useful, have patterns to add or would like advice on building your own accessible forms, find me on LinkedIn and let's chat.
Top comments (0)