Accessibility (a11y) of Expo apps is an important aspect to ensure all users, including those with visual, hearing, motor, or cognitive impairments, can effectively use the app. Expo and React Native provide rich accessibility APIs and properties.
Accessibility Basics:
- accessibilityLabel
Provide element descriptions for screen readers.
typescript<Image source={{ uri: 'https://example.com/image.jpg' }} accessibilityLabel="User avatar" style={{ width: 50, height: 50 }} /> <Button title="Submit" accessibilityLabel="Submit form" onPress={handleSubmit} />
- accessibilityHint
Provide additional information about element behavior.
typescript<TouchableOpacity accessibilityLabel="View details" accessibilityHint="Tap to view detailed user information" onPress={handlePress} > <Text>View</Text> </TouchableOpacity>
- accessibilityRole
Specify the UI role of an element.
typescript<View accessibilityRole="button" accessibilityLabel="Confirm" onClick={handleConfirm} > <Text>Confirm</Text> </View>
Common Accessibility Roles:
button: Buttonlink: Linkheader: Headertext: Textimage: Imagesearch: Search boxadjustable: Adjustable control
- accessibilityState
Describe the current state of an element.
typescript<CheckBox accessibilityLabel="Agree to terms" accessibilityState={{ checked: isChecked, disabled: false, }} value={isChecked} onValueChange={setIsChecked} />
Accessibility States:
disabled: Disabled stateselected: Selected statechecked: Checked statebusy: Busy stateexpanded: Expanded state
- accessibilityValue
Describe the value of an element.
typescript<Slider accessibilityLabel="Volume" accessibilityValue={{ min: 0, max: 100, now: volume }} value={volume} onValueChange={setVolume} /> <ProgressBar accessibilityLabel="Download progress" accessibilityValue={{ min: 0, max: 100, now: progress }} progress={progress / 100} />
Accessibility Actions:
- accessibilityActions
Define accessibility actions supported by an element.
typescript<View accessibilityLabel="Playback controls" accessibilityActions={[ { name: 'increment', label: 'Increase volume' }, { name: 'decrement', label: 'Decrease volume' }, { name: 'magicTap', label: 'Double tap to play/pause' }, ]} onAccessibilityAction={(event) => { switch (event.nativeEvent.actionName) { case 'increment': setVolume((v) => Math.min(v + 10, 100)); break; case 'decrement': setVolume((v) => Math.max(v - 10, 0)); break; case 'magicTap': togglePlayPause(); break; } }} > <Text>Volume: {volume}</Text> </View>
- onAccessibilityEscape
Define escape action.
typescript<Modal visible={isVisible} onAccessibilityEscape={() => setIsVisible(false)} accessibilityViewIsModal={true} > <View> <Text>Modal content</Text> <Button title="Close" onPress={() => setIsVisible(false)} /> </View> </Modal>
Accessibility Properties:
- accessible
Mark an element as accessible.
typescript<View accessible={true}> <Text>This view can be accessed by screen readers</Text> </View>
- accessibilityElementsHidden
Hide accessibility of child elements.
typescript<View accessible={true} accessibilityLabel="Container" accessibilityElementsHidden={isHidden} > <Text>Child element 1</Text> <Text>Child element 2</Text> </View>
- accessibilityIgnoresInvertColors
Ignore color inversion settings.
typescript<Image source={{ uri: 'https://example.com/chart.jpg' }} accessibilityIgnoresInvertColors={true} style={{ width: 200, height: 200 }} />
Focus Management:
- focusable
Make an element focusable.
typescript<TextInput focusable={true} accessibilityLabel="Username input" placeholder="Enter username" />
- accessibilityLiveRegion
Mark dynamic content regions.
typescript<Text accessibilityLiveRegion="polite" accessibilityLabel="Status message" > {statusMessage} </Text>
Accessibility Events:
typescriptfunction useAccessibilityFocus() { const [isFocused, setIsFocused] = useState(false); const handleFocus = () => { setIsFocused(true); console.log('Element focused'); }; const handleBlur = () => { setIsFocused(false); console.log('Element blurred'); }; return { isFocused, handleFocus, handleBlur }; }
Semantic Components:
- Use Semantic HTML Tags (Web)
typescript// Use semantic tags on web platform if (Platform.OS === 'web') { return ( <nav accessibilityRole="navigation"> <ul> <li><a href="/home">Home</a></li> <li><a href="/about">About</a></li> </ul> </nav> ); }
- Use Correct Accessibility Roles
typescript// Use button role for buttons <TouchableOpacity accessibilityRole="button" accessibilityLabel="Submit" onPress={handleSubmit} > <Text>Submit</Text> </TouchableOpacity> // Use link role for links <TouchableOpacity accessibilityRole="link" accessibilityLabel="View details" onPress={handlePress} > <Text>View details</Text> </TouchableOpacity>
Best Practices:
- Provide Clear Labels
typescript// Good practice <Button title="Submit form" accessibilityLabel="Submit user registration form" onPress={handleSubmit} /> // Avoid duplication <Button title="Submit" accessibilityLabel="Submit" // Duplicates title onPress={handleSubmit} />
- Use Meaningful Hints
typescript<TouchableOpacity accessibilityLabel="Delete item" accessibilityHint="This action cannot be undone, proceed with caution" onPress={handleDelete} > <Text>Delete</Text> </TouchableOpacity>
- Support Keyboard Navigation
typescriptfunction KeyboardNavigation() { const [focusedIndex, setFocusedIndex] = useState(0); const handleKeyDown = (event) => { if (event.key === 'ArrowDown') { setFocusedIndex((i) => Math.min(i + 1, items.length - 1)); } else if (event.key === 'ArrowUp') { setFocusedIndex((i) => Math.max(i - 1, 0)); } else if (event.key === 'Enter') { items[focusedIndex].onPress(); } }; return ( <View onKeyDown={handleKeyDown}> {items.map((item, index) => ( <TouchableOpacity key={index} accessibilityLabel={item.label} focusable={true} style={[ styles.item, focusedIndex === index && styles.focused, ]} onPress={item.onPress} > <Text>{item.label}</Text> </TouchableOpacity> ))} </View> ); }
- Support Screen Readers
typescriptfunction ScreenReaderSupport() { const isScreenReaderEnabled = useAccessibilityInfo(); return ( <View> {isScreenReaderEnabled ? ( <Text>Screen reader enabled</Text> ) : ( <Text>Screen reader not enabled</Text> )} </View> ); }
- Test Accessibility
typescriptimport { AccessibilityInfo } from 'react-native'; async function testAccessibility() { // Check if screen reader is enabled const isScreenReaderEnabled = await AccessibilityInfo.isScreenReaderEnabled(); console.log('Screen reader enabled:', isScreenReaderEnabled); // Check reduce motion setting const isReduceMotionEnabled = await AccessibilityInfo.isReduceMotionEnabled(); console.log('Reduce motion enabled:', isReduceMotionEnabled); // Listen for accessibility changes AccessibilityInfo.addEventListener( 'screenReaderChanged', (isEnabled) => { console.log('Screen reader changed:', isEnabled); } ); }
Accessibility Tools:
- AccessibilityInfo API
typescriptimport { AccessibilityInfo } from 'react-native'; // Get accessibility info const isScreenReaderEnabled = await AccessibilityInfo.isScreenReaderEnabled(); const isReduceMotionEnabled = await AccessibilityInfo.isReduceMotionEnabled(); // Listen for changes AccessibilityInfo.addEventListener('screenReaderChanged', (isEnabled) => { console.log('Screen reader:', isEnabled); }); AccessibilityInfo.addEventListener('reduceMotionChanged', (isEnabled) => { console.log('Reduce motion:', isEnabled); });
- Accessibility Testing Tools
- iOS: VoiceOver
- Android: TalkBack
- Web: Screen readers (NVDA, JAWS, etc.)
Common Accessibility Issues:
-
Missing Accessibility Labels
- Add accessibilityLabel to all interactive elements
- Provide descriptive labels for images
-
Poor Focus Management
- Ensure keyboard navigation order is logical
- Provide clear focus indicators
-
Insufficient Color Contrast
- Ensure text and background have sufficient contrast
- Support high contrast mode
-
Dynamic Content Not Announced
- Use accessibilityLiveRegion to mark dynamic content
- Promptly notify screen readers of content changes
By implementing these accessibility practices, you can ensure Expo apps are friendly and usable for all users.