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.
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.
Every UI component must expose its role and label to assistive technologies. This is achieved through the following patterns:
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"
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);
}
}
All interactive elements must be navigable using only a keyboard or external input device. Focus must be visible and follow a predictable order.
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>
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" />
Text must scale seamlessly across all font sizes, including system-wide dynamic type.
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);
}
}
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.
Text must meet minimum contrast ratios (4.5:1 for normal text, 3:1 for large text) across all states and themes.
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)")
}
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>
Accessibility must be tested across real devices and assistive technologies.
Use native screen readers: VoiceOver on iOS, TalkBack on Android.
Verify that:
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()));
}
Common silent failures include:
accessibilityLabel values (e.g., "Button 1" instead of "Add new item").accessibilityHint or accessibilityValue.FocusScope or ScrollView after navigation.accessibilityTraits (e.g., button without selected when it should be toggled).ScrollView or RecyclerView.Failures appear silently when:
TextView because it lacks accessibilityRole.To diagnose, use:
accessibilityDebug on iOS (UIAccessibility.isVoiceOverRunning and UIAccessibility.isReduceMotionEnabled).adb shell dumpsys accessibility on Android to inspect current state.When accessibility fails, these diagnostic messages are common:
accessibilityFocused state is not applied.accessibilityLabel do not align.accessibilityLabel or contentDescription is missing.ScrollView does not preserve focus.accessibilityLabel and accessibilityHint on interactive elements.accessibilityRole to define the semantic meaning of components (e.g., table, list, menu).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.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy