UTA DevHub
UI Development/UI Architecture/Foundation Components Guide

Accessibility Patterns

Comprehensive accessibility implementation including roles, states, focus management, and testing strategies for inclusive component design.

Accessibility Patterns

Overview

Accessibility ensures that all users, regardless of their abilities, can effectively use your application. React Native provides built-in accessibility features that, when properly implemented, create inclusive experiences for users with visual, motor, or cognitive disabilities.

Accessibility is Not Optional

Making your components accessible:

  • Expands your user base to include users with disabilities
  • Improves usability for all users in various contexts
  • Ensures legal compliance with accessibility regulations
  • Demonstrates social responsibility and inclusive design
  • Often improves overall app quality and structure

Core Accessibility Concepts

The Accessibility Tree

React Native creates an accessibility tree parallel to the view hierarchy. Screen readers and other assistive technologies use this tree to understand and interact with your app.

// Regular view tree
<View>
  <Text>Hello</Text>
  <Button onPress={handlePress}>Click me</Button>
</View>
 
// Accessibility tree representation
<group>
  <text>Hello</text>
  <button>Click me</button>
</group>

Platform Differences

iOS VoiceOver Specifics

  • Gestures: Swipe to navigate, double-tap to activate
  • Rotor: Two-finger rotation to change navigation mode
  • Magic Tap: Two-finger double-tap for primary action
  • Escape: Two-finger Z gesture to go back
// iOS-specific accessibility
<View
  accessibilityRole="button"
  accessibilityLabel="Save document"
  accessibilityHint="Double tap to save your changes"
  accessibilityActions={[
    { name: 'magicTap', label: 'Quick save' }
  ]}
  onAccessibilityAction={(event) => {
    if (event.actionName === 'magicTap') {
      quickSave();
    }
  }}
/>

Required Accessibility Features

Every foundation component must implement these accessibility features:

1. Semantic Roles

// Button component
<TouchableOpacity
  accessibilityRole="button"
  accessibilityLabel="Save changes"
  accessibilityHint="Saves your current changes to the profile"
>
 
// Text Input component
<TextInput
  accessibilityRole="text"
  accessibilityLabel="Email address"
  accessibilityHint="Enter your email address for account recovery"
>
 
// Image component
<Image
  accessibilityRole="image"
  accessibilityLabel="User profile photo"
  alt="Profile photo of John Smith"
>
 
// Link component
<TouchableOpacity
  accessibilityRole="link"
  accessibilityLabel="Terms of Service"
  accessibilityHint="Opens terms of service in browser"
>

2. State Communication

// Button states
<TouchableOpacity
  accessibilityState={{
    disabled: isDisabled,
    busy: isLoading,
    selected: isSelected,
  }}
>
 
// Input states
<TextInput
  accessibilityState={{
    disabled: isDisabled,
    invalid: hasError,
  }}
>
 
// Expandable states
<TouchableOpacity
  accessibilityState={{
    expanded: isExpanded,
  }}
  accessibilityLabel={`Settings menu ${isExpanded ? 'expanded' : 'collapsed'}`}
>
 
// Multi-state components
<View
  accessibilityState={{
    checked: isChecked,
    disabled: isDisabled,
    busy: isProcessing,
  }}
>

3. Dynamic Content

// Live region for dynamic updates
<View accessibilityLiveRegion="polite">
  <Text>{searchResultCount} results found</Text>
</View>
 
// Assertive announcements for urgent updates
<View accessibilityLiveRegion="assertive">
  <Text>Error: Invalid email address</Text>
</View>
 
// None for decorative content
<View accessibilityLiveRegion="none">
  <AnimatedBackground />
</View>

Focus Management

Focus Control

Programmatic Focus

import { findNodeHandle, AccessibilityInfo } from 'react-native';
 
const MyComponent = () => {
  const inputRef = useRef(null);
  
  const focusInput = () => {
    const handle = findNodeHandle(inputRef.current);
    if (handle) {
      AccessibilityInfo.setAccessibilityFocus(handle);
    }
  };
  
  return (
    <View>
      <Button onPress={focusInput} title="Focus input" />
      <TextInput ref={inputRef} />
    </View>
  );
};

Focus Order

// Control tab order with accessibilityViewIsModal
<Modal>
  <View accessibilityViewIsModal={true}>
    {/* Focus trapped within modal */}
    <TextInput accessibilityLabel="Name" />
    <TextInput accessibilityLabel="Email" />
    <Button title="Submit" />
  </View>
</Modal>
 
// Group related elements
<View accessible={true} accessibilityLabel="User information">
  <Text>John Doe</Text>
  <Text>john@example.com</Text>
</View>

Focus Restoration

const MyList = () => {
  const [items, setItems] = useState([...]);
  const focusedItemRef = useRef(null);
  
  const deleteItem = (index: number) => {
    // Store reference to next item
    if (index < items.length - 1) {
      focusedItemRef.current = items[index + 1].id;
    }
    
    setItems(items.filter((_, i) => i !== index));
  };
  
  useEffect(() => {
    // Restore focus after deletion
    if (focusedItemRef.current) {
      const handle = findNodeHandle(refs[focusedItemRef.current]);
      AccessibilityInfo.setAccessibilityFocus(handle);
      focusedItemRef.current = null;
    }
  }, [items]);
};

Accessibility Testing

Testing Patterns

// ui/foundation/Button/Button.test.tsx
import { render, fireEvent } from '@testing-library/react-native';
import { Button } from './Button';
 
describe('Button Accessibility', () => {
  it('has correct accessibility role', () => {
    const { getByRole } = render(<Button>Test</Button>);
    expect(getByRole('button')).toBeTruthy();
  });
 
  it('communicates disabled state', () => {
    const { getByRole } = render(<Button disabled>Test</Button>);
    const button = getByRole('button');
    expect(button.props.accessibilityState.disabled).toBe(true);
  });
 
  it('communicates loading state', () => {
    const { getByRole } = render(<Button loading>Test</Button>);
    const button = getByRole('button');
    expect(button.props.accessibilityState.busy).toBe(true);
  });
 
  it('has accessible label', () => {
    const { getByLabelText } = render(
      <Button accessibilityLabel="Save changes">Save</Button>
    );
    expect(getByLabelText('Save changes')).toBeTruthy();
  });
  
  it('provides hint for complex actions', () => {
    const { getByRole } = render(
      <Button 
        accessibilityHint="Double tap to save all changes"
      >
        Save All
      </Button>
    );
    const button = getByRole('button');
    expect(button.props.accessibilityHint).toBe(
      'Double tap to save all changes'
    );
  });
});

Component-Specific Patterns

Form Components

// Accessible form field
export const FormField = ({ label, error, ...props }) => {
  const inputId = useId();
  const errorId = useId();
  
  return (
    <View>
      <Text nativeID={inputId}>{label}</Text>
      <TextInput
        accessibilityLabel={label}
        accessibilityLabelledBy={inputId}
        accessibilityDescribedBy={error ? errorId : undefined}
        accessibilityState={{ invalid: !!error }}
        {...props}
      />
      {error && (
        <Text
          nativeID={errorId}
          accessibilityRole="alert"
          accessibilityLiveRegion="assertive"
          style={styles.error}
        >
          {error}
        </Text>
      )}
    </View>
  );
};

List Components

// Accessible list with actions
export const AccessibleList = ({ items, onDelete }) => {
  return (
    <FlatList
      accessibilityRole="list"
      data={items}
      renderItem={({ item, index }) => (
        <View
          accessibilityRole="listitem"
          accessible={true}
          accessibilityLabel={`${item.name}, ${index + 1} of ${items.length}`}
          accessibilityActions={[
            { name: 'delete', label: 'Delete item' }
          ]}
          onAccessibilityAction={(event) => {
            if (event.actionName === 'delete') {
              onDelete(item.id);
            }
          }}
        >
          <Text>{item.name}</Text>
        </View>
      )}
    />
  );
};
// Accessible modal
export const AccessibleModal = ({ visible, onClose, children }) => {
  const previousFocusRef = useRef(null);
  
  useEffect(() => {
    if (visible) {
      // Store current focus
      previousFocusRef.current = findNodeHandle(
        AccessibilityInfo.currentlyFocusedField()
      );
    } else if (previousFocusRef.current) {
      // Restore focus when closing
      AccessibilityInfo.setAccessibilityFocus(previousFocusRef.current);
    }
  }, [visible]);
  
  return (
    <Modal
      visible={visible}
      onRequestClose={onClose}
      accessible={true}
      accessibilityViewIsModal={true}
    >
      <View>
        <View accessible={true} accessibilityRole="header">
          <Text>Modal Title</Text>
          <TouchableOpacity
            accessibilityRole="button"
            accessibilityLabel="Close modal"
            onPress={onClose}
          >
            <Icon name="close" />
          </TouchableOpacity>
        </View>
        {children}
      </View>
    </Modal>
  );
};

Best Practices Summary

Accessibility Excellence

Remember these key principles:

  • Test with real assistive technologies - Simulators aren't enough
  • Provide context - Labels should make sense out of context
  • Be consistent - Similar elements should behave similarly
  • Avoid redundancy - Don't repeat visual information
  • Design for everyone - Accessibility benefits all users

(Do ✅) Accessibility best practices

  • Always provide accessibilityLabel for interactive elements
  • Use semantic accessibilityRole values
  • Communicate state changes with accessibilityState
  • Test with actual screen readers on devices
  • Provide hints for complex interactions
  • Group related content with accessible={true}
  • Use accessibilityLiveRegion for dynamic content
  • Maintain logical focus order
  • Provide text alternatives for images
  • Ensure sufficient color contrast (4.5:1 minimum)

Next Steps