Mobile App Accessibility

Build mobile apps that work for everyone. Covers screen reader support, dynamic type, color contrast, touch target sizing, and the accessibility patterns that make native apps usable by the 1 billion people worldwide with disabilities.

Mobile App Accessibility ensures that apps are usable by people with a wide range of abilities, including visual, auditory, motor, and cognitive impairments. It matters when users interact with an app using only voice, touch, keyboard, or screen reader, and when the app must support dynamic text sizes, high-contrast modes, and screen reader navigation. It is not a post-launch polish but a foundational requirement for inclusive design.

Core Principles of Accessibility

Accessibility is not a checklist but a set of interlocking behaviors that must be implemented consistently across all app components. The core principles are:

These principles are realized through platform-specific APIs and standard web and native conventions, with no single tooling suite dominating the space.

Implementing Semantic Structure

Every UI component must expose its role and label to assistive technologies. This is achieved through the following patterns:

Native Elements with Built-in Semantics

Use platform-native controls where possible. On iOS, use UIButton, UILabel, UISwitch, and UITableView; on Android, use Button, TextView, Switch, and RecyclerView. These controls automatically provide semantic roles and accessible properties.

For example, in Android XML:

<Button
    android:id="@+id/settings_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Settings"
    android:contentDescription="Open app settings"
    android:accessibilityLiveRegion="polite" />

On iOS, configure accessibility in code:

let settingsButton = UIButton(type: .system)
settingsButton.setTitle("Settings", for: .normal)
settingsButton.accessibilityLabel = "Open app settings"
settingsButton.accessibilityTraits = [.button, .selected]
settingsButton.accessibilityHint = "Taps to open the settings screen"

Custom Components with Manual Semantics

For custom views, manually set accessibility properties using standard APIs.

In React Native, use the AccessibilityProps interface:

<TouchableOpacity
  accessibilityLabel="Add new task"
  accessibilityRole="button"
  accessibilityState={{ pressed: true }}
  accessibilityHint="Press to create a new to-do item"
  style={styles.button}
  onPress={addTask}
>
  <Text>Add Task</Text>
</TouchableOpacity>

On Android, extend View and override onInitializeAccessibilityNodeInfo:

public class IconButton extends View {
    private String label;
    private boolean isSelected;

    public IconButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        // ...
    }

    @Override
    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfo(info);
        info.setClassName(IconButton.class.getSimpleName());
        info.setPackageName(getContext().getPackageName());
        info.setText(label);
        info.setClickable(true);
        info.setCheckable(true);
        info.setCheckable(true);
        info.setChecked(isSelected);
    }

    public void setLabel(String label) {
        this.label = label;
        // Notify accessibility system
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
    }
}

Keyboard Navigation and Focus Management

All interactive elements must be navigable using only a keyboard or external input device. Focus must be visible and follow a predictable order.

Tab Order and Focus Indicators

Ensure tab order is logical and consistent. On iOS, use UIAccessibility.isVoiceOverRunning to detect when VoiceOver is active and adjust focus behavior accordingly.

In Flutter, use FocusScope to manage focus:

FocusScope(
  child: Column(
    children: [
      TextFormField(
        decoration: InputDecoration(labelText: 'Name'),
        autofocus: true,
      ),
      TextFormField(
        decoration: InputDecoration(labelText: 'Email'),
        focusNode: emailFocusNode,
      ),
      ElevatedButton(
        onPressed: submitForm,
        child: Text('Submit'),
      ),
    ],
  ),
  focusNode: formFocusScope,
);

On Android, enable focusable views with android:focusable="true" and android:clickable="true", and define a custom focus order:

<LinearLayout
    android:id="@+id/form_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:descendantFocusability="afterDescendants">

    <EditText
        android:id="@+id/name_field"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Full name"
        android:focusable="true"
        android:clickable="true" />

    <EditText
        android:id="@+id/email_field"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Email address"
        android:focusable="true"
        android:clickable="true" />

</LinearLayout>

Focus Highlighting

Use accessibilityFocused and accessibilityCurrent states to indicate current focus. On iOS, ensure UIAccessibility.isVoiceOverRunning triggers visual focus rings.

For a custom focus ring in Android, use a StateListDrawable:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_focused="true">
        <shape android:shape="rectangle">
            <stroke android:width="2dp" android:color="#3366CC" />
            <corners android:radius="8dp" />
        </shape>
    </item>
    <item>
        <shape android:shape="rectangle">
            <stroke android:width="1dp" android:color="#CCCCCC" />
            <corners android:radius="8dp" />
        </shape>
    </item>
</selector>

Apply this to a Button:

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Next"
    android:background="@drawable/focus_ring"
    android:padding="12dp" />

Dynamic Text and Layout Adaptation

Text must scale seamlessly across all font sizes, including system-wide dynamic type.

Supporting Dynamic Text Sizes

On iOS, use UIFontMetrics to adapt font sizes:

let titleFont = UIFont.preferredFont(forTextStyle: .title1)
let scaledFont = UIFontMetrics(forTextStyle: .title1).scaledFont(for: titleFont)
titleLabel.font = scaledFont

On Android, use TypedValue.applyDimension and Configuration to handle dynamic type:

public class DynamicTextHelper {
    public static float getSpForTextSize(Context context, float sizeInSp) {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP,
            sizeInSp,
            context.getResources().getDisplayMetrics()
        );
    }

    public static void updateTextSize(TextView view) {
        Configuration config = view.getResources().getConfiguration();
        float size = config.fontScale * 16; // base size in sp
        view.setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
    }
}

Layout Constraints for Scalable UI

Use flexible layout systems. In SwiftUI, use HStack, VStack, and ZStack with Spacer and GeometryReader:

VStack(spacing: 16) {
    Text("Welcome to the app")
        .font(.largeTitle)
        .multilineTextAlignment(.center)

    Text("This screen adapts to different text sizes and screen sizes.")
        .font(.body)
        .lineLimit(nil)
        .padding()

    Button("Get Started") {
        // action
    }
    .buttonStyle(.borderedProminent)
}
.padding()

On Android, use ConstraintLayout with app:layout_constraintHorizontal_weight and app:layout_constraintVertical_weight for responsive layouts.

Color and Contrast Requirements

Text must meet minimum contrast ratios (4.5:1 for normal text, 3:1 for large text) across all states and themes.

Achieving Minimum Contrast

Use ColorUtils.calculateContrast in Android:

float contrast = ColorUtils.calculateContrast(
    Color.parseColor("#FFFFFF"), // text color
    Color.parseColor("#000000")  // background color
);
if (contrast < 4.5) {
    Log.w("Accessibility", "Text contrast too low: " + contrast);
}

On iOS, use UIColor and UIAccessibility:

let textColor = UIColor.white
let backgroundColor = UIColor.black
let contrastRatio = UIColor.contrastRatio(for: textColor, in: backgroundColor)
if contrastRatio < 4.5 {
    print("Text contrast too low: \(contrastRatio)")
}

Supporting High-Contrast and Dark Mode

Ensure all elements adapt to system-wide contrast settings.

On iOS, use UIAccessibility.isReduceTransparencyEnabled and UIAccessibility.isHighContrastEnabled:

if UIAccessibility.isReduceTransparencyEnabled {
    view.backgroundColor = UIColor.systemBackground
} else {
    view.backgroundColor = UIColor.systemBackground.withAlphaComponent(0.8)
}

if UIAccessibility.isHighContrastEnabled {
    titleLabel.font = UIFont.preferredFont(forTextStyle: .headline).withWeight(.bold)
    titleLabel.textColor = UIColor.white
    titleLabel.backgroundColor = UIColor.black
}

On Android, apply android:theme="@style/Theme.AppCompat.Light.DarkActionBar" and define values-night resources:

<!-- res/values-night/styles.xml -->
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.DayNight">
        <item name="android:windowBackground">@color/black</item>
        <item name="android:textColor">@color/white</item>
        <item name="android:colorBackground">@color/black</item>
        <item name="android:colorControlNormal">@color/white</item>
        <item name="android:colorControlActivated">@color/white</item>
    </style>
</resources>

Testing and Validation

Accessibility must be tested across real devices and assistive technologies.

Manual Testing with Screen Readers

Use native screen readers: VoiceOver on iOS, TalkBack on Android.

Verify that:

Automated Testing with Platform Tools

Use UIAutomation on iOS and UiAutomator on Android.

In iOS, use XCTest with XCTAssert and XCUIElement:

let app = XCUIApplication()
app.launch()

let settingsButton = app.buttons["Settings"]
XCTAssertTrue(settingsButton.exists, "Settings button should be present")
XCTAssertEqual(settingsButton.label, "Settings", "Button label should match")

settingsButton.tap()
XCTAssertTrue(app.staticTexts["Settings"].exists, "Settings screen should appear")

On Android, use Espresso with onView and check:

@Test
public void testSettingsButtonAccessibility() {
    onView(withText("Settings"))
        .check(matches(isDisplayed()))
        .check(matches(withContentDescription("Open app settings")))
        .check(matches(hasAccessibilityDescription("Open app settings")))
        .check(matches(isClickable()));
}

Debugging Accessibility Failures

Common silent failures include:

Failures appear silently when:

To diagnose, use:

Error Texts and Diagnostics

When accessibility fails, these diagnostic messages are common:

Best Practices Summary

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.