← Back to Blog Our Take

Enhancing Accessibility in React Native: A Practical Guide with Code Examples

December 16, 2024 By Karly Lamm
Enhancing Accessibility in React Native: A Practical Guide with Code Examples

Building accessible mobile apps isn’t just about compliance, it’s about creating inclusive experiences that work for everyone. In this guide, I’ll walk you through practical accessibility enhancements for a React Native To-Do app, showing you exactly how to implement features that make a real difference.

Why Accessibility Matters in Mobile Apps

Over 1 billion people worldwide live with disabilities, and mobile devices are often their primary way of accessing digital services. Yet many React Native apps are built without considering screen readers, voice control, or other assistive technologies. This isn’t just a missed opportunity, it’s a barrier that excludes a significant portion of users.

The good news? React Native provides excellent accessibility APIs that, when used correctly, can create rich, inclusive experiences with minimal additional code.

Starting with the Basics: A Simple To-Do App

Let’s start with a basic To-Do component and progressively enhance it:

// Initial implementation - not accessible
const TodoItem = ({ item, onToggle, onDelete }) => {
  return (
    <View style={styles.container}>
      <TouchableOpacity onPress={() => onToggle(item.id)}>
        <Text>{item.completed ? '✓' : '○'}</Text>
      </TouchableOpacity>
      <Text style={item.completed ? styles.completed : styles.text}>
        {item.text}
      </Text>
      <TouchableOpacity onPress={() => onDelete(item.id)}>
        <Text>🗑️</Text>
      </TouchableOpacity>
    </View>
  );
};

This component works visually, but it’s completely inaccessible to screen reader users. Let’s fix that.

Enhancement 1: Semantic Labels and Roles

The first step is adding proper accessibility labels and roles:

const TodoItem = ({ item, onToggle, onDelete }) => {
  return (
    <View 
      style={styles.container}
      accessible={true}
      accessibilityRole="listitem"
    >
      <TouchableOpacity 
        onPress={() => onToggle(item.id)}
        accessible={true}
        accessibilityRole="checkbox"
        accessibilityState={{ checked: item.completed }}
        accessibilityLabel={`Mark "${item.text}" as ${item.completed ? 'incomplete' : 'complete'}`}
      >
        <Text>{item.completed ? '✓' : '○'}</Text>
      </TouchableOpacity>
      
      <Text 
        style={item.completed ? styles.completed : styles.text}
        accessible={true}
        accessibilityRole="text"
        accessibilityState={{ selected: item.completed }}
      >
        {item.text}
      </Text>
      
      <TouchableOpacity 
        onPress={() => onDelete(item.id)}
        accessible={true}
        accessibilityRole="button"
        accessibilityLabel={`Delete "${item.text}"`}
        accessibilityHint="Double tap to delete this item"
      >
        <Text>🗑️</Text>
      </TouchableOpacity>
    </View>
  );
};

Enhancement 2: Dynamic Announcements

When users interact with the app, they need feedback. React Native’s AccessibilityInfo API allows us to provide dynamic announcements:

import { AccessibilityInfo } from 'react-native';

const TodoApp = () => {
  const [todos, setTodos] = useState([]);

  const handleToggle = (id) => {
    setTodos(prevTodos => {
      const updatedTodos = prevTodos.map(todo => {
        if (todo.id === id) {
          const updated = { ...todo, completed: !todo.completed };
          
          // Announce the change to screen readers
          AccessibilityInfo.announceForAccessibility(
            `${updated.text} marked as ${updated.completed ? 'complete' : 'incomplete'}`
          );
          
          return updated;
        }
        return todo;
      });
      
      return updatedTodos;
    });
  };

  const handleDelete = (id) => {
    const todoToDelete = todos.find(todo => todo.id === id);
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
    
    // Announce deletion
    AccessibilityInfo.announceForAccessibility(
      `"${todoToDelete.text}" deleted`
    );
  };

  // ... rest of component
};

Enhancement 3: Focus Management

Proper focus management is crucial for keyboard and switch control users:

import { useRef, useEffect } from 'react';

const AddTodoForm = ({ onAdd }) => {
  const inputRef = useRef(null);
  const [text, setText] = useState('');

  const handleSubmit = () => {
    if (text.trim()) {
      onAdd(text.trim());
      setText('');
      
      // Keep focus on input for easy addition of multiple items
      setTimeout(() => {
        inputRef.current?.focus();
      }, 100);
      
      // Announce successful addition
      AccessibilityInfo.announceForAccessibility(
        `"${text.trim()}" added to your to-do list`
      );
    }
  };

  return (
    <View style={styles.form}>
      <TextInput
        ref={inputRef}
        value={text}
        onChangeText={setText}
        placeholder="Add a new task..."
        style={styles.input}
        accessible={true}
        accessibilityLabel="New task input"
        accessibilityHint="Enter the text for your new to-do item"
        returnKeyType="done"
        onSubmitEditing={handleSubmit}
      />
      <TouchableOpacity 
        onPress={handleSubmit}
        style={styles.addButton}
        accessible={true}
        accessibilityRole="button"
        accessibilityLabel="Add task"
        accessibilityState={{ disabled: !text.trim() }}
      >
        <Text style={styles.addButtonText}>Add</Text>
      </TouchableOpacity>
    </View>
  );
};

Enhancement 4: Reduced Motion Preferences

Respect users’ motion preferences to prevent discomfort or vestibular disorders:

import { AccessibilityInfo } from 'react-native';
import { useEffect, useState } from 'react';

const AnimatedTodoItem = ({ item, onToggle, onDelete }) => {
  const [reduceMotion, setReduceMotion] = useState(false);

  useEffect(() => {
    // Check if user prefers reduced motion
    AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);
    
    // Listen for changes
    const subscription = AccessibilityInfo.addEventListener(
      'reduceMotionChanged',
      setReduceMotion
    );

    return () => subscription?.remove();
  }, []);

  const animationConfig = reduceMotion 
    ? { duration: 0 } // No animation
    : { duration: 300 }; // Normal animation

  return (
    <Animated.View
      style={[
        styles.container,
        {
          opacity: item.completed ? 0.6 : 1,
          transform: [{
            scale: item.completed 
              ? (reduceMotion ? 1 : 0.95)
              : 1
          }]
        }
      ]}
      // ... rest of component
    >
      {/* Component content */}
    </Animated.View>
  );
};

Enhancement 5: Screen Reader Optimization

Group related elements and provide context for screen reader users:

const TodoList = ({ todos, onToggle, onDelete }) => {
  return (
    <View
      accessible={true}
      accessibilityRole="list"
      accessibilityLabel={`To-do list with ${todos.length} items`}
    >
      {todos.length === 0 ? (
        <Text 
          style={styles.emptyMessage}
          accessible={true}
          accessibilityRole="text"
          accessibilityLiveRegion="polite"
        >
          No tasks yet. Add one above to get started!
        </Text>
      ) : (
        todos.map((item, index) => (
          <TodoItem
            key={item.id}
            item={item}
            onToggle={onToggle}
            onDelete={onDelete}
            accessibilityLabel={`Task ${index + 1} of ${todos.length}: ${item.text}`}
          />
        ))
      )}
    </View>
  );
};

Testing Your Accessibility Implementation

1. Enable Screen Reader Testing

// Add this to your development builds for easy testing
const DevelopmentAccessibilityTools = () => {
  if (__DEV__) {
    return (
      <View style={styles.devTools}>
        <TouchableOpacity
          onPress={() => {
            AccessibilityInfo.isScreenReaderEnabled().then(enabled => {
              console.log('Screen reader enabled:', enabled);
            });
          }}
          style={styles.testButton}
        >
          <Text>Test Screen Reader Status</Text>
        </TouchableOpacity>
      </View>
    );
  }
  return null;
};

2. Accessibility Inspector

On iOS, use the Accessibility Inspector in Xcode. On Android, use TalkBack or the Accessibility Scanner.

Best Practices Summary

  1. Always provide meaningful labels: Don’t rely on visual cues alone
  2. Use semantic roles: Help assistive technologies understand your interface
  3. Manage focus appropriately: Guide users through your app logically
  4. Provide dynamic feedback: Announce important state changes
  5. Respect user preferences: Honor reduced motion and other accessibility settings
  6. Test with real assistive technologies: Nothing beats testing with actual screen readers

The Impact

After implementing these accessibility features, our To-Do app:

  • Works seamlessly with VoiceOver and TalkBack
  • Provides clear navigation for switch control users
  • Respects user motion preferences
  • Offers rich, contextual feedback for all interactions

Accessibility isn’t a checkbox to tick, it’s an ongoing commitment to inclusive design. Start with these patterns, test with real users, and remember that every accessibility improvement makes your app better for everyone.

The extra time invested in accessibility pays dividends in user satisfaction, app store ratings, and the knowledge that your app truly serves all users, regardless of their abilities.

Ready to Build Something Amazing?

Let's discuss how Aviron Labs can help bring your ideas to life with custom software solutions.

Get in Touch