Accessibility in Mobile Apps: Building for All Users
Approximately 4.4 million Australians have a disability. That is roughly 18 percent of the population. Many of these people use smartphones daily, relying on assistive technologies to navigate apps. If your app is not accessible, you are excluding a significant portion of potential users.
Beyond the moral argument, accessibility is increasingly a legal consideration. The Australian Disability Discrimination Act 1992 and the Web Content Accessibility Guidelines (WCAG) establish expectations for digital accessibility. Major organisations have faced legal action for inaccessible digital products.
This guide covers practical techniques for building accessible mobile apps on iOS and Android. These are not theoretical principles but implementable code patterns that make a real difference.
Understanding Assistive Technologies
Screen Readers
VoiceOver (iOS) and TalkBack (Android) are the primary screen readers. They read aloud the content on screen, allowing visually impaired users to navigate by swiping between elements and tapping to activate.
When a user navigates to an element, the screen reader announces:
- The element’s label (what it is)
- Its trait/role (button, heading, image)
- Its value or state (selected, disabled, 50 percent)
- Any hint (double-tap to activate)
Switch Control
Users with limited motor control may use external switches to navigate. The switch moves focus sequentially through interactive elements. Every interactive element must be focusable and have a clear label.
Dynamic Type / Font Scaling
Users with low vision increase system font sizes. Your app must support this gracefully, reflowing layouts rather than clipping text.
Voice Control
iOS Voice Control and Android Voice Access let users operate their devices entirely by voice. Interactive elements need visible labels that match their accessibility labels.
Access
ibility Labels
Every interactive element and meaningful image needs an accessibility label. This is the most impactful single improvement you can make.
iOS (Swift/SwiftUI)
// UIKit
button.accessibilityLabel = "Create new project"
button.accessibilityHint = "Opens a form to create a new project"
profileImage.accessibilityLabel = "Sarah's profile photo"
profileImage.isAccessibilityElement = true
// SwiftUI
Button(action: createProject) {
Image(systemName: "plus")
}
.accessibilityLabel("Create new project")
.accessibilityHint("Opens a form to create a new project")
Image("profile-sarah")
.accessibilityLabel("Sarah's profile photo")
Android (Kotlin)
// View-based
button.contentDescription = "Create new project"
profileImage.contentDescription = "Sarah's profile photo"
// Jetpack Compose
IconButton(
onClick = { createProject() },
modifier = Modifier.semantics {
contentDescription = "Create new project"
}
) {
Icon(Icons.Filled.Add, contentDescription = null)
}
React Native
<TouchableOpacity
onPress={createProject}
accessible={true}
accessibilityLabel="Create new project"
accessibilityHint="Opens a form to create a new project"
accessibilityRole="button"
>
<Icon name="plus" />
</TouchableOpacity>
<Image
source={require('./profile-sarah.png')}
accessible={true}
accessibilityLabel="Sarah's profile photo"
/>
Label Writing Guidelines
Do: Be concise and descriptive. “Delete project” not “Button to delete this project.”
Do: Describe the action, not the appearance. “Search” not “Magnifying glass icon.”
Do: Include state information. “Favourite, selected” or “Task completed.”
Do not: Include the element type. Screen readers announce “button” automatically. “Delete project button” is redundant.
Do not: Use vague labels. “Button 1” or “Image” is useless.
Do not: Include “tap to” or “click to.” Screen readers provide their own interaction hints.
Sema
ntic Structure
Headings
Mark headings as headings. Screen reader users navigate by headings to skim content, similar to how sighted users scan visually.
// SwiftUI
Text("Project Details")
.font(.title)
.accessibilityAddTraits(.isHeader)
// UIKit
label.accessibilityTraits = .header
// Compose
Text(
"Project Details",
modifier = Modifier.semantics { heading() },
style = MaterialTheme.typography.h5
)
// React Native
<Text
style={styles.heading}
accessibilityRole="header"
>
Project Details
</Text>
Lists
Announce list context so users know they are navigating a list:
// SwiftUI: Lists are accessible by default
List {
ForEach(projects) { project in
ProjectRow(project: project)
}
}
// VoiceOver announces "row 1 of 5" etc.
Grouping Related Elements
Group related elements so they are announced as a single unit:
// SwiftUI: Combine elements
HStack {
Image(systemName: "checkmark.circle.fill")
VStack(alignment: .leading) {
Text("Design mockups")
Text("Due tomorrow")
}
}
.accessibilityElement(children: .combine)
// VoiceOver: "checkmark circle fill, Design mockups, Due tomorrow"
// Or provide a custom label:
.accessibilityElement(children: .ignore)
.accessibilityLabel("Design mockups, completed, due tomorrow")
// React Native: Group elements
<View
accessible={true}
accessibilityLabel="Design mockups, completed, due tomorrow"
>
<Icon name="check-circle" />
<Text>Design mockups</Text>
<Text>Due tomorrow</Text>
</View>
Dynamic Ty
pe Support
iOS Dynamic Type
Support Dynamic Type so text scales with the user’s preferred size:
// SwiftUI: Use built-in text styles (automatic Dynamic Type support)
Text("Project Title")
.font(.headline) // Scales automatically
Text("Last updated 3 hours ago")
.font(.caption) // Scales automatically
// UIKit: Use preferred fonts
label.font = UIFont.preferredFont(forTextStyle: .headline)
label.adjustsFontForContentSizeCategory = true
For custom fonts:
// UIKit: Scale custom fonts
let customFont = UIFont(name: "Avenir-Medium", size: 16)!
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: customFont)
label.adjustsFontForContentSizeCategory = true
Layout considerations:
- Use Auto Layout constraints that adapt to content size
- Never set fixed heights on text containers
- Allow text to wrap to multiple lines
- Test at the largest accessibility text size (Accessibility Extra Extra Extra Large)
Android Font Scaling
Android scales text automatically when you use sp units. Ensure your layouts accommodate larger text:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:maxLines="3"
android:ellipsize="end" />
In Compose:
Text(
text = "Project Title",
style = MaterialTheme.typography.h6, // Uses sp by default
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Colour and Contrast
Minimum Contrast Ratios
WCAG 2.1 Level AA requires:
- Normal text: 4.5:1 contrast ratio
- Large text (18pt+ or 14pt+ bold): 3:1 contrast ratio
- Non-text elements (icons, borders): 3:1 contrast ratio
Testing Contrast
iOS: Xcode’s Accessibility Inspector includes a colour contrast checker.
Android: Android Studio’s Layout Inspector shows accessibility warnings for insufficient contrast.
Online tools: WebAIM’s contrast checker (webaim.org/resources/contrastchecker) works for verifying specific colour pairs.
Do Not Rely on Colour Alone
Never use colour as the only way to convey information:
BAD: Red text means error, green text means success (colour-blind users cannot distinguish)
GOOD: Red text with an error icon and "Error:" prefix. Green text with a checkmark and "Success:" prefix.
// Good: Icon + colour + text
HStack {
Image(systemName: "exclamationmark.circle.fill")
.foregroundColor(.red)
Text("Error: Please enter a valid email")
.foregroundColor(.red)
}
.accessibilityElement(children: .combine)
Dark Mode
Both iOS and Android support dark mode. Ensure your colour palette works in both modes:
- Use semantic colours that adapt automatically
- Test all screens in both light and dark mode
- Verify contrast ratios in both modes
// SwiftUI: Adaptive colours
Text("Primary text")
.foregroundColor(.primary) // Adapts to light/dark
Text("Secondary text")
.foregroundColor(.secondary) // Adapts to light/dark
Touch Targets
Ensure all interactive elements meet minimum size requirements:
- iOS: 44x44 points
- Android: 48x48 dp
// SwiftUI: Ensure minimum touch target
Button(action: { /* action */ }) {
Image(systemName: "trash")
.font(.system(size: 16))
}
.frame(minWidth: 44, minHeight: 44)
// Compose: Ensure minimum touch target
IconButton(
onClick = { /* action */ },
modifier = Modifier.size(48.dp)
) {
Icon(Icons.Default.Delete, contentDescription = "Delete")
}
Spacing between interactive elements should be at least 8 points to prevent accidental taps.
Motion and Animations
Some users experience motion sickness or vestigo from animations. Both platforms provide a “Reduce Motion” setting.
// SwiftUI: Respect reduce motion
@Environment(\.accessibilityReduceMotion) var reduceMotion
var body: some View {
content
.animation(reduceMotion ? .none : .easeInOut, value: isExpanded)
}
// UIKit
if UIAccessibility.isReduceMotionEnabled {
// Use simple fade instead of slide animation
}
// Android: Check animation settings
val animationScale = Settings.Global.getFloat(
contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
1f
)
if (animationScale == 0f) {
// Animations are disabled
}
Form Accessibility
Forms are particularly important for accessibility. Poorly labelled forms are a common barrier.
Label Every Input
// SwiftUI
TextField("Email address", text: $email)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.accessibilityLabel("Email address")
Announce Errors Clearly
When form validation fails, announce the error to screen readers:
// iOS: Post accessibility notification
UIAccessibility.post(
notification: .announcement,
argument: "Error: Email address is invalid"
)
// React Native: Announce error
import { AccessibilityInfo } from 'react-native';
AccessibilityInfo.announceForAccessibility('Error: Email address is invalid');
Input Types
Set the correct keyboard type and content type for each field:
TextField("Phone number", text: $phone)
.keyboardType(.phonePad)
.textContentType(.telephoneNumber)
This helps all users, not just those using assistive technologies.
Testing Accessibility
Manual Testing
Test your app with each platform’s screen reader:
iOS VoiceOver:
- Enable in Settings, then Accessibility, then VoiceOver
- Navigate your entire app using swipe gestures
- Verify every element is announced meaningfully
- Complete all primary user tasks without looking at the screen
Android TalkBack:
- Enable in Settings, then Accessibility, then TalkBack
- Navigate using swipe and explore-by-touch
- Verify all elements are announced correctly
- Complete primary tasks using only TalkBack
Automated Testing
Use accessibility audit tools:
iOS: Xcode’s Accessibility Inspector scans for missing labels, insufficient contrast, and small touch targets.
Android: Android Studio’s Layout Inspector flags accessibility issues. The Accessibility Scanner app provides on-device audits.
Accessibility Audit Checklist
- Every interactive element has a meaningful accessibility label
- Headings are marked with the heading trait/role
- Related elements are grouped appropriately
- Dynamic Type is supported (text scales with system settings)
- Colour contrast meets WCAG 2.1 AA requirements
- Information is not conveyed by colour alone
- Touch targets meet platform minimums (44pt iOS, 48dp Android)
- Reduce Motion preference is respected
- Form inputs are labelled and errors are announced
- The app is fully navigable with a screen reader
- Dark mode maintains adequate contrast
Making the Case for Accessibility
If you need to convince stakeholders that accessibility is worth the investment:
- Market size: 4.4 million Australians with disabilities represent a substantial addressable market
- Legal risk: The Disability Discrimination Act applies to digital products
- Broader benefits: Accessibility improvements (clear labels, good contrast, large touch targets) benefit all users
- App Store advantage: Apple highlights accessible apps, and users mention accessibility in positive reviews
- Retention: Users who rely on accessibility features are often highly loyal to apps that support them well
Accessibility is not a bolt-on feature. It is a quality of well-built software. At eawesome, we build accessibility into every mobile project from the start, because including all users is not a feature; it is a responsibility.