Mobile Accessibility

Production engineering guide for mobile accessibility covering patterns, implementation strategies, and operational best practices.

Mobile Accessibility is the practice of ensuring that mobile applications are usable by people with a wide range of abilities, including visual, auditory, motor, and cognitive impairments. It matters when a user taps, swipes, or speaks to navigate an app that must work reliably across devices, screen sizes, operating systems, and input modalities—especially when users cannot rely on sight or fine motor control. It is not optional; it is foundational.

Core Principles of Mobile Accessibility

Semantic Structure and Navigation

Mobile accessibility begins with clear, semantic structure. A screen must convey its purpose and navigation path through native platform semantics. Use accessibilityLabel, accessibilityHint, and accessibilityRole on UI components to expose meaning to screen readers and assistive technologies.

On iOS, use UIAccessibility APIs directly:

button.accessibilityLabel = "Submit order"
button.accessibilityHint = "Taps to confirm and send your order to the kitchen."
button.accessibilityRole = .button

On Android, use AccessibilityNodeInfo and ViewCompat:

ViewCompat.setAccessibilityHeading(button, true);
button.setContentDescription("Submit order");
button.setAccessibilityHint("Taps to confirm and send your order to the kitchen.");

The most common failure: accessibilityLabel is used but accessibilityHint is omitted. Screen reader users expect hints to explain *why* a button does what it does, not just *what* it does. A button labeled "Save" with no hint is often misunderstood as "save current form" without context.

Focus Management and Touch Targets

Touch targets must be at least 44×44 points (iOS) or 48×48 dp (Android). Use accessibilityFocus and accessibilityState to manage focus across dynamic content.

On iOS, when a modal sheet appears:

sheetViewController.view.accessibilityElements = [titleView, textField, submitButton]
sheetViewController.view.accessibilityElementsHidden = false
sheetViewController.view.accessibilityFrame = CGRect(x: 0, y: 0, width: 320, height: 400)

On Android, use AccessibilityEvent to notify when focus changes:

AccessibilityEvent event = AccessibilityEvent.obtain();
event.setEventType(AccessibilityEvent.TYPE_VIEW_FOCUSED);
event.setPackageName("com.example.app");
event.setClassName("com.example.app.CheckoutActivity");
event.setEventTime(SystemClock.uptimeMillis());
event.setSource(button);
AccessibilityManager.getInstance(context).notifyAccessibilityEvent(event);

The silent failure: focus is set on a ListView item, but no accessibilityFocus is triggered. Users swipe to a new item, but the screen reader does not announce the new focus. Ensure accessibilityFocused is set to true on the focused item, and that accessibilityFocus is enabled on the parent container.

Dynamic Content and Live Regions

When content changes dynamically—e.g., a chat message appears, a form field updates—use accessibilityLiveRegion to announce updates without requiring user navigation.

On iOS:

chatView.accessibilityLiveRegion = .polite
chatView.accessibilityElements = [messageLabel, timestampLabel]

On Android:

chatView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
chatView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);

The most common error: using accessibilityLiveRegion = .polite but failing to trigger accessibilityStateChanged on the container when new content arrives. The screen reader waits for the user to navigate, but the update is not announced immediately.

To trigger updates:

let event = UIAccessibilityNotificationEvent(
    notification: .announcement,
    message: "New message from Sarah: 'Hello, how are you?'"
)
UIAccessibility.post(notification: event)

On Android:

AccessibilityEvent event = AccessibilityEvent.obtain();
event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
event.setPackageName("com.example.app");
event.getText().add("New message from Sarah: 'Hello, how are you?'");
AccessibilityManager.getInstance(context).notifyAccessibilityEvent(event);

Voice and Input Modalities

Voice navigation requires predictable, discoverable actions. Use accessibilityActions to expose custom gestures or commands.

On iOS, define a custom action:

button.accessibilityActions = [
    UIAccessibilityCustomAction(name: "Toggle Favorite", handler: { _, _ in
        self.toggleFavorite()
    })
]

On Android, use AccessibilityNodeInfo to register actions:

AccessibilityNodeInfo nodeInfo = AccessibilityNodeInfo.obtain();
nodeInfo.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, "Toggle Favorite"));

The key failure: a Button has accessibilityActions, but the AccessibilityService does not dispatch ACTION_CLICK when the button is tapped. Ensure that AccessibilityService is registered and that onAccessibilityEvent handles TYPE_VIEW_CLICKED.

Color and Contrast

Text must meet WCAG 2.1 AA contrast ratio (4.5:1) between text and background. Use accessibilityContrast and accessibilityTraits to support high-contrast modes.

On iOS:

label.accessibilityTraits = .staticText | .header
label.accessibilityContrast = .high

On Android:

ViewCompat.setAccessibilityContrastRatio(textView, 4.5f);
textView.setTextAppearance(context, R.style.HighContrastText);

The silent failure: high-contrast mode is enabled, but accessibilityContrast is not set. The app appears in high contrast, but the screen reader does not announce the contrast mode or apply it to dynamic content.

Dynamic Text and Layouts

Text must scale with user preferences. Use accessibilityContentSizeCategory to adapt layout to extraSmall, small, medium, large, extraLarge, double, triple.

On iOS:

label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true

On Android:

textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
textView.setAutoSizeTextTypeWithDefaults(AutoSizeTextType.TEXT_SIZE_AUTO);

The most subtle failure: adjustsFontForContentSizeCategory is set, but accessibilityContentSizeCategory is not updated when the user changes system font size. Ensure UIContentSizeCategoryDidChange is observed:

NotificationCenter.default.addObserver(
    self,
    selector: #selector(handleContentSizeChange),
    name: UIContentSizeCategory.didChangeNotification,
    object: nil
)

func handleContentSizeChange() {
    let category = UIContentSizeCategory.current
    label.font = UIFont.preferredFont(forTextStyle: .body)
    label.adjustsFontForContentSizeCategory = true
    label.font = UIFont.preferredFont(forTextStyle: .body)
}

On Android:

Configuration config = getResources().getConfiguration();
config.fontScale = 1.5f;
getResources().updateConfiguration(config, getResources().getDisplayMetrics());

Gestures and Touch Feedback

Complex gestures must be discoverable and feedback-rich. Use accessibilityGesture and accessibilityFeedback to announce and provide haptic or audio feedback.

On iOS:

scrollView.accessibilityTraits = .scrollable
scrollView.accessibilityScrollDirection = .horizontal
scrollView.accessibilityFeedbackGenerator = UISelectionFeedbackGenerator()
scrollView.accessibilityGesture = .swipe

On Android:

scrollView.setAccessibilityFeedbackType(View.ACCESSIBILITY_FEEDBACK_HAPTIC);
scrollView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);

The failure: a ListView scrolls, but no accessibilityScroll announcement is triggered. The user swipes, but the screen reader does not announce the scroll direction or the current item.

To fix:

scrollView.accessibilityScrollDirection = .horizontal
scrollView.accessibilityScrollPosition = 0.75
scrollView.accessibilityScrollTitle = "Shopping List: 3 of 7 items"

On Android:

AccessibilityEvent event = AccessibilityEvent.obtain();
event.setEventType(AccessibilityEvent.TYPE_VIEW_SCROLLED);
event.setScrollY(120);
event.setScrollX(0);
event.setScrollY(120);
event.getText().add("Scrolled to item 3 of 7");
AccessibilityManager.getInstance(context).notifyAccessibilityEvent(event);

Common Failure Modes

Missing or Misused Labels

Focus and Navigation

Dynamic Updates

Text and Layouts

Testing Mobile Accessibility

Test on real devices with screen readers enabled. Use built-in tools:

For automated testing, use accessibility inspector tools:

  xcrun simctl launch booted com.example.app --args --UITest
  xcrun xcuitest -app /path/to/app.app -test /path/to/test.xctest
  adb shell am start -n com.example.app/.MainActivity
  adb shell dumpsys accessibility
  adb shell dumpsys window windows | grep -A 10 "Focused Window"

Use AccessibilityNodeInfo to extract the full accessibility tree:

adb shell dumpsys accessibility | grep -A 10 "Node"

Common test cases:

Summary

Mobile accessibility is not a checklist—it is a system. It requires consistent use of semantic structure, focus management, dynamic updates, and input feedback across all layers of the app. The most effective apps announce changes, adapt to user preferences, and support multiple input modalities without requiring the user to learn new patterns. When these elements are in place, the app becomes truly accessible. When one fails silently, the user stumbles.

This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.