Back to Homepage

International Standards

Back to Homepage
Best Practices

Mobile Accessibility Best Practices

A comprehensive guide to building accessible iOS and Android applications - covering native apps, hybrid frameworks, touch targets, gestures, screen readers, and platform-specific accessibility features

Mobile-First Accessibility
With over 60% of web traffic coming from mobile devices and billions of people relying solely on mobile for internet access, mobile accessibility is not optional. WCAG 2.1 introduced 17 new criteria specifically addressing mobile needs. This guide covers both native apps and mobile web experiences.

Introduction to Mobile Accessibility

Why Mobile Accessibility Matters

Mobile devices have become the primary way billions of people access digital content and services. For many users with disabilities, mobile devices with built-in accessibility features like screen readers, voice control, and magnification are more affordable and accessible than traditional assistive technologies that require expensive desktop software or hardware.

Key mobile accessibility considerations:

  • Small screens: Limited space requires careful prioritization and organization
  • Touch interfaces: Touch targets must be large enough and appropriately spaced
  • Context of use: Mobile users are often on-the-go, in varying lighting conditions, or multitasking
  • Platform diversity: iOS and Android have different accessibility APIs and screen readers
  • Motion and gestures: Complex gestures may be difficult for users with motor disabilities
  • Changing abilities: Environmental factors (bright sunlight, noise) create temporary disabilities

iOS vs Android Accessibility

Both major mobile platforms provide robust accessibility features, but they differ in implementation:

Feature iOS Android
Screen Reader VoiceOver (built-in) TalkBack (built-in)
Activation Triple-click Home/Side button or Settings Volume keys or Settings
Gestures Swipe right/left to navigate, double-tap to activate Swipe right/left to navigate, double-tap to activate
Voice Control Voice Control (separate from Siri) Voice Access
Magnification Zoom (3-finger double-tap) Magnification (triple-tap or button)
Display Options Display Accommodations, Larger Text Font size, Display size, Color correction
Switch Control Switch Control Switch Access

Mobile Accessibility Standards

Mobile apps must comply with:

  • WCAG 2.1 Level AA: Functional requirements apply to mobile (with platform-appropriate implementations)
  • EN 301 549: European standard includes mobile app requirements
  • Section 508: US federal standard updated to include mobile
  • Platform guidelines: iOS Human Interface Guidelines and Android Material Design include accessibility requirements

iOS Native App Accessibility

UIKit Accessibility

Making Elements Accessible

All UI elements should be accessible to VoiceOver. UIKit controls are accessible by default, but custom views need explicit configuration.

// Swift - Making a custom view accessible
class CustomButton: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupAccessibility()
    }

    func setupAccessibility() {
        // Make the view accessible
        isAccessibilityElement = true

        // Set accessibility label (what the element is)
        accessibilityLabel = "Submit form"

        // Set accessibility trait (what kind of element)
        accessibilityTraits = .button

        // Optional: Set accessibility hint (what it does)
        accessibilityHint = "Double tap to submit the form"

        // Optional: Set accessibility value (current state)
        // accessibilityValue = "Not submitted"
    }
}

Accessibility Labels and Hints

Best practices for labels and hints:

  • Label: Brief, descriptive noun phrase (e.g., "Add to cart", not "Button that adds item to cart")
  • Hint: Describes the result of an action, only when not obvious from label
  • Value: Current state or value (e.g., "Selected", "3 of 5 stars")
  • Don't include element type in label (VoiceOver announces this via traits)
  • Keep labels concise - users navigate by swiping through many elements
// Good examples
button.accessibilityLabel = "Play"
button.accessibilityHint = "Starts playback"

slider.accessibilityLabel = "Volume"
slider.accessibilityValue = "\(Int(slider.value * 100))%"

// Bad examples
button.accessibilityLabel = "Play button" // "button" is redundant
button.accessibilityHint = "Button" // Not helpful
textField.accessibilityLabel = "Type here" // Should describe what to type

Accessibility Traits

Traits tell VoiceOver what kind of element it is and how to interact with it.

// Common accessibility traits
.button          // "Button" - double tap to activate
.link            // "Link" - opens content
.searchField     // "Search field" - brings up keyboard
.image           // "Image" - non-interactive image
.staticText      // Plain text
.header          // Heading (navigable with rotor)
.selected        // "Selected" - current state
.notEnabled      // "Dimmed" - disabled state
.adjustable      // Can be incremented/decremented with swipe up/down

// Combining traits
view.accessibilityTraits = [.button, .selected]

// Custom controls
view.accessibilityTraits = .adjustable
view.accessibilityValue = "Medium"

// Implementing adjustable
override func accessibilityIncrement() {
    // User swiped up
    increaseValue()
}

override func accessibilityDecrement() {
    // User swiped down
    decreaseValue()
}

Grouping Elements

Group related elements so VoiceOver treats them as a single item, reducing navigation complexity.

// Group a card with image, title, and description
cardView.isAccessibilityElement = true
cardView.accessibilityLabel = "\(title), \(description)"
cardView.accessibilityTraits = .button

// Hide child elements from VoiceOver
imageView.isAccessibilityElement = false
titleLabel.isAccessibilityElement = false
descriptionLabel.isAccessibilityElement = false

SwiftUI Accessibility

SwiftUI provides declarative modifiers for accessibility:

// SwiftUI accessibility modifiers
Button(action: submit) {
    Text("Submit")
}
.accessibilityLabel("Submit form")
.accessibilityHint("Double tap to submit")
.accessibilityAddTraits(.isButton)

// Grouping
HStack {
    Image("star")
    Text("4.5 stars")
    Text("(120 reviews)")
}
.accessibilityElement(children: .combine)
.accessibilityLabel("Rating: 4.5 stars, 120 reviews")

// Custom actions
Text("Article title")
    .accessibilityActions {
        Button("Share") { shareArticle() }
        Button("Bookmark") { bookmarkArticle() }
    }

// Value and adjustable
Slider(value: $volume, in: 0...100)
    .accessibilityLabel("Volume")
    .accessibilityValue("\(Int(volume))%")

// Hidden elements
Image(decorative: "background")
    .accessibilityHidden(true)

Testing with VoiceOver

How to test with VoiceOver:

  1. Enable VoiceOver: Settings → Accessibility → VoiceOver (or triple-click Home/Side button)
  2. Basic gestures:
    • Swipe right: Next element
    • Swipe left: Previous element
    • Double-tap: Activate element
    • Three-finger swipe: Scroll
    • Two-finger double-tap: Magic tap (primary action)
    • Rotor: Two-finger twist to access navigation shortcuts
  3. What to check:
    • Can you navigate to all interactive elements?
    • Are labels clear and concise?
    • Do elements announce their type correctly?
    • Is the navigation order logical?
    • Are decorative elements properly hidden?
    • Do custom controls work with swipe up/down?
Accessibility Inspector
Use Xcode's Accessibility Inspector to audit your app:
  • Xcode → Open Developer Tool → Accessibility Inspector
  • Run automated audits
  • Inspect element hierarchy
  • Test with simulated VoiceOver

Android Native App Accessibility

Android Accessibility Basics

Content Descriptions

Content descriptions are the Android equivalent of accessibility labels.

<!-- XML -->
<ImageButton
    android:id="@+id/playButton"
    android:src="@drawable/ic_play"
    android:contentDescription="@string/play_button" />

<!-- Decorative images -->
<ImageView
    android:src="@drawable/decorative"
    android:importantForAccessibility="no" />

// Kotlin - Setting programmatically
button.contentDescription = "Play audio"

// Dynamic content description
button.contentDescription = if (isPlaying) {
    "Pause audio"
} else {
    "Play audio"
}

Labeling Form Fields

<!-- Using android:hint -->
<EditText
    android:id="@+id/emailInput"
    android:hint="Email address"
    android:inputType="textEmailAddress" />

<!-- Using android:labelFor for separate labels -->
<TextView
    android:id="@+id/passwordLabel"
    android:text="Password"
    android:labelFor="@id/passwordInput" />

<EditText
    android:id="@+id/passwordInput"
    android:inputType="textPassword" />

Custom Accessibility Actions

// Kotlin - Adding custom actions
ViewCompat.addAccessibilityAction(
    view,
    "Share"
) { view, arguments ->
    shareContent()
    true
}

ViewCompat.addAccessibilityAction(
    view,
    "Bookmark"
) { view, arguments ->
    bookmarkContent()
    true
}

// User can access these via TalkBack's Actions menu

Grouping and Focus Order

<!-- Group related content -->
<LinearLayout
    android:screenReaderFocusable="true"
    android:contentDescription="Product: Phone, $599, 4.5 stars">

    <ImageView
        android:src="@drawable/phone"
        android:importantForAccessibility="no" />

    <TextView
        android:text="Phone"
        android:importantForAccessibility="no" />

    <TextView
        android:text="$599"
        android:importantForAccessibility="no" />
</LinearLayout>

<!-- Control focus order -->
<View
    android:id="@+id/firstElement"
    android:nextFocusForward="@+id/secondElement" />

Jetpack Compose Accessibility

// Compose accessibility modifiers
Button(
    onClick = { submit() },
    modifier = Modifier.semantics {
        contentDescription = "Submit form"
        role = Role.Button
    }
) {
    Text("Submit")
}

// Merge descendant semantics
Row(
    modifier = Modifier.semantics(mergeDescendants = true) {
        contentDescription = "4.5 stars, 120 reviews"
    }
) {
    Icon(Icons.Filled.Star, contentDescription = null)
    Text("4.5")
    Text("(120 reviews)")
}

// Custom actions
Text(
    "Article title",
    modifier = Modifier.semantics {
        customActions = listOf(
            CustomAccessibilityAction("Share") {
                shareArticle()
                true
            },
            CustomAccessibilityAction("Bookmark") {
                bookmarkArticle()
                true
            }
        )
    }
)

// State descriptions
Checkbox(
    checked = isChecked,
    onCheckedChange = { isChecked = it },
    modifier = Modifier.semantics {
        stateDescription = if (isChecked) "Checked" else "Unchecked"
    }
)

Testing with TalkBack

How to test with TalkBack:

  1. Enable TalkBack: Settings → Accessibility → TalkBack (or Volume keys shortcut)
  2. Basic gestures:
    • Swipe right: Next element
    • Swipe left: Previous element
    • Double-tap: Activate element
    • Two-finger swipe: Scroll
    • Reading controls menu: Swipe down then right
  3. Testing checklist:
    • All elements have content descriptions
    • Form fields have labels
    • Focus order is logical
    • Custom actions work correctly
    • State changes are announced
    • Decorative elements are hidden
Accessibility Scanner
Use Google's Accessibility Scanner app to identify accessibility issues:
  • Install from Google Play Store
  • Grant permissions
  • Scan any screen in your app
  • Review suggestions for improvement

Touch Targets and Gestures

Touch Target Size

WCAG 2.1 Success Criterion 2.5.5 (Level AAA) requires touch targets to be at least 44×44 CSS pixels. Both iOS and Android recommend minimum 44×44 points (iOS) / 48×48 dp (Android).

/* iOS - Minimum touch target */
button.frame(minWidth: 44, minHeight: 44)

// Android - Minimum touch target
<Button
    android:minWidth="48dp"
    android:minHeight="48dp" />

// If visual size must be smaller, increase touch area
view.frame(width: 24, height: 24)
    .contentShape(Rectangle())
    .padding(10) // Creates 44×44 touch area

Touch target best practices:

  • Provide at least 8 pixels (8pt/8dp) spacing between adjacent touch targets
  • Make the entire button/link area tappable, not just the text or icon
  • In compact layouts (toolbars), maintain minimum size even if it requires spacing adjustments
  • Consider users with motor impairments, arthritis, or Parkinson's disease

Gesture Accessibility

Provide Alternatives to Complex Gestures

WCAG 2.1 requires alternatives to path-based and multi-point gestures (2.5.1).

  • Path-based: Gestures requiring specific path (swipe shapes, drawing) need simpler alternatives
  • Multi-point: Pinch-to-zoom, multi-finger swipes need one-finger alternatives
  • Multi-tap: Double-tap, triple-tap should have single-tap alternatives
// GOOD: Pinch to zoom with buttons
ZoomableImage()
    .zoomButtons() // Provides +/- buttons as alternative

// GOOD: Swipe to delete with button
List {
    ForEach(items) { item in
        ItemRow(item)
            .swipeActions {
                Button(role: .destructive) {
                    delete(item)
                } label: {
                    Label("Delete", systemImage: "trash")
                }
            }
    }
}

// BAD: Required complex gesture with no alternative
// Drawing signature with no "Type name" option

Motion Actuation

Provide alternatives to motion-based controls (shaking, tilting) per WCAG 2.5.4.

// GOOD: Shake to undo with button alternative
.onShake {
    undo()
}
// Also provide:
Button("Undo", action: undo)

// GOOD: Respect motion preferences
if !accessibilityPrefersReducedMotion {
    // Use motion-based feature
} else {
    // Use static alternative
}

Responsive Mobile Design

Text Sizing and Reflow

Support user text size preferences (WCAG 1.4.4 - Resize text, 1.4.10 - Reflow).

// iOS - Dynamic Type
Text("Hello")
    .font(.body) // Automatically scales with user settings

// Custom fonts with scaling
    .font(.custom("MyFont", size: 17, relativeTo: .body))

// Android - Scalable text
<TextView
    android:text="Hello"
    android:textSize="16sp" /> <!-- Use sp, not dp -->

// Compose
Text(
    "Hello",
    fontSize = 16.sp // Scales with user settings
)

Text scaling best practices:

  • Use platform dynamic text (Dynamic Type on iOS, sp units on Android)
  • Test with largest text size (Settings → Display → Text Size)
  • Ensure layouts reflow gracefully, don't truncate
  • Consider horizontal scrolling for very large text if needed
  • Don't use fixed heights that cause text clipping

Orientation Support

Support both portrait and landscape orientations unless a specific orientation is essential (WCAG 1.3.4).

  • Don't lock orientation unless absolutely necessary (e.g., specific AR experiences)
  • Ensure all content and functionality work in both orientations
  • Adapt layout intelligently to available space
  • Users with mounted devices may need a specific orientation

Color Contrast on Mobile

Mobile devices are often used outdoors in bright sunlight, making contrast even more critical.

  • Follow WCAG 2.1 contrast requirements: 4.5:1 for normal text, 3:1 for large text
  • Test in bright sunlight conditions
  • Support dark mode with appropriate contrast ratios
  • Don't rely on color alone to convey information
// iOS - Dark mode support
Color("PrimaryText") // Defined in asset catalog with light/dark variants

Text("Important")
    .foregroundColor(.primary) // Automatically adapts

// Android - Dark mode
<resources>
    <color name="primaryText">#000000</color>
</resources>

<!-- res/values-night/colors.xml -->
<resources>
    <color name="primaryText">#FFFFFF</color>
</resources>

Hybrid and Cross-Platform Apps

React Native Accessibility

import { View, Text, TouchableOpacity } from 'react-native';

<TouchableOpacity
  accessible={true}
  accessibilityLabel="Submit form"
  accessibilityHint="Double tap to submit"
  accessibilityRole="button"
  onPress={handleSubmit}>
  <Text>Submit</Text>
</TouchableOpacity>

// Hide decorative elements
<Image
  source={require('./decoration.png')}
  accessibilityElementsHidden={true} // iOS
  importantForAccessibility="no" // Android
/>

// Group elements
<View
  accessible={true}
  accessibilityLabel="Product: Phone, $599, 4.5 stars">
  <Image source={productImage} />
  <Text>Phone</Text>
  <Text>$599</Text>
  <Text>4.5 ★</Text>
</View>

Flutter Accessibility

// Flutter accessibility
Semantics(
  label: 'Submit form',
  hint: 'Double tap to submit',
  button: true,
  child: ElevatedButton(
    onPressed: submit,
    child: Text('Submit'),
  ),
)

// Merge semantics
Semantics(
  label: '4.5 stars, 120 reviews',
  child: Row(
    children: [
      Icon(Icons.star),
      Text('4.5'),
      Text('(120 reviews)'),
    ],
  ),
)

// Exclude from semantics
ExcludeSemantics(
  child: Image.asset('decoration.png'),
)
Test on Real Devices
Always test on physical devices with actual screen readers (VoiceOver/TalkBack). Simulators and emulators don't fully replicate the screen reader experience. Test with various text sizes, color schemes, and accessibility settings enabled.