Accessibility on Android: TalkBack, Semantics & Touch Targets
Build apps that work for every user — and pass accessibility audits in senior interviews
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Android accessibility is the set of APIs, guidelines, and tools that make apps usable by people with visual, motor, auditory, or cognitive impairments. It is implemented through the AccessibilityNodeInfo system in Views and the Semantics system in Jetpack Compose, surfaced to assistive technologies like TalkBack, Switch Access, and Voice Access.
Real-world relevance
Google Maps, Gmail, and WhatsApp invest heavily in accessibility. WhatsApp labels every voice message play button with a content description including the contact name and duration. Google Maps announces 'Directions to [destination], 12 minutes' as a live region update. Apps that skip this lose 15% of the global population who have some form of disability.
Key points
- TalkBack — Android's primary screen reader — TalkBack is a system service that reads UI elements aloud and provides gesture navigation for blind and low-vision users. It uses the AccessibilityNodeInfo tree — a mirror of the View hierarchy or Compose semantic tree — to announce element labels, roles, and states.
- contentDescription in Views — For ImageView, ImageButton, and any view without visible text, set android:contentDescription in XML or view.contentDescription in code. TalkBack reads this string. Empty string ('') signals 'decorative, skip me.' Null means TalkBack guesses — often wrong.
- Compose Modifier.semantics{} — In Compose, semantics replace contentDescription. Use Modifier.semantics { contentDescription = 'Delete item' } for custom descriptions, and mergeDescendants = true to merge a card's children into one focusable unit. Compose auto-generates semantics for Text, Button, Checkbox — you only override when the default is wrong.
- Touch target minimum — 48dp — Android accessibility guidelines (and Material Design) require interactive elements to have a minimum touch target of 48x48dp. A 24dp icon inside a button can have a 48dp clickable area via Modifier.minimumInteractiveComponentSize() or padding. Google Play's pre-launch report flags targets smaller than 48dp.
- Focus order and traversal — TalkBack moves focus in view order by default. In Views, use android:accessibilityTraversalAfter/Before to override the order. In Compose, use Modifier.semantics { traversalIndex = -1f } to move a composable earlier in the focus order (lower index = earlier).
- Live regions — dynamic content announcement — When content changes without user interaction (a loading indicator becomes a result, an error appears), use ViewCompat.setAccessibilityLiveRegion(view, ACCESSIBILITY_LIVE_REGION_POLITE) in Views, or Modifier.semantics { liveRegion = LiveRegionMode.Polite } in Compose. POLITE waits for the current announcement to finish; ASSERTIVE interrupts.
- Heading semantics — Screen reader users navigate by headings. Mark section titles with ViewCompat.setAccessibilityHeading(view, true) in Views, or Modifier.semantics { heading() } in Compose. This lets users jump between sections without traversing every element.
- State announcements — checked, selected, disabled — Compose Button, Checkbox, Switch, and RadioButton all emit correct state semantics automatically. For custom components, declare state explicitly: Modifier.semantics { stateDescription = if (isOn) 'On' else 'Off' }. Avoid using color alone to communicate state.
- Testing — Accessibility Scanner and UI Automator — The Accessibility Scanner app (by Google) overlays issues on your running app: missing labels, small touch targets, low contrast. In automated testing, use ComposeTestRule.onNode(hasContentDescription('Delete')).assertIsDisplayed() or UiAutomator with UiSelector.description() to assert accessibility properties.
- Contrast ratio — WCAG AA minimum 4.5:1 — Body text must have a contrast ratio of at least 4.5:1 against its background (WCAG AA). Large text (18sp+) requires 3:1. Material 3's color system is built to satisfy these ratios by default, but custom colors must be checked. Android Studio's Layout Inspector shows contrast ratios.
- Custom actions — For swipe-to-delete or long-press actions that TalkBack cannot replicate, expose them as custom accessibility actions: ViewCompat.addAccessibilityAction(view, 'Delete', ...). In Compose: Modifier.semantics { customActions = listOf(CustomAccessibilityAction('Delete') { ... }) }.
- Audit before submission — Run Accessibility Scanner on every screen before release. Google Play's pre-launch report includes an accessibility check. Apps on Google Play that fail basic accessibility may be flagged in accessibility-focused app stores and reviews. Senior engineers treat accessibility as a first-class requirement, not an afterthought.
Code example
// ── Compose: accessible product card ─────────────────────────
@Composable
fun ProductCard(
product: Product,
onDelete: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.semantics(mergeDescendants = true) {} // merge child semantics
.clickable { /* open detail */ }
.padding(16.dp)
) {
AsyncImage(
model = product.imageUrl,
contentDescription = null, // decorative; card semantics cover it
modifier = Modifier.size(48.dp)
)
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f)) {
Text(
text = product.name,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.semantics { heading() }
)
Text(
text = product.price,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
IconButton(
onClick = onDelete,
modifier = Modifier
.minimumInteractiveComponentSize() // enforces 48dp touch target
.semantics {
contentDescription = "Delete ${product.name}"
customActions = listOf(
CustomAccessibilityAction("Delete") { onDelete(); true }
)
}
) {
Icon(Icons.Default.Delete, contentDescription = null)
}
}
}
// ── Live region for status updates ───────────────────────────
@Composable
fun SyncStatus(message: String) {
Text(
text = message,
modifier = Modifier.semantics {
liveRegion = LiveRegionMode.Polite
}
)
}Line-by-line walkthrough
- 1. semantics(mergeDescendants = true) on the Row collapses all child semantic nodes into one — TalkBack reads the card as a single focused item, not a dozen separate elements
- 2. AsyncImage with contentDescription = null marks the image as decorative — the merged Row semantics already describe the product
- 3. Modifier.semantics { heading() } on the product name Text marks it as a heading — TalkBack users can swipe to jump between headings without traversing every element
- 4. minimumInteractiveComponentSize() on the IconButton enforces the 48dp touch target rule even when the visible icon is 24dp
- 5. contentDescription on the IconButton semantics block overrides the default 'button' announcement with the specific product name — critical for lists with many delete buttons
- 6. customActions exposes 'Delete' as an accessibility action — TalkBack users in 'explore by touch' mode can activate it from the actions menu instead of needing to precisely tap the icon
- 7. SyncStatus Text with liveRegion = LiveRegionMode.Polite announces status changes (e.g. 'Sync complete') to TalkBack after the current speech finishes — no extra user gesture needed
- 8. Icon inside the IconButton has contentDescription = null — the parent button's semantics already carry the description; double-announcing would confuse TalkBack
Spot the bug
@Composable
fun FavoriteButton(isFavorite: Boolean, onClick: () -> Unit) {
IconButton(onClick = onClick) {
Icon(
imageVector = if (isFavorite) Icons.Filled.Favorite
else Icons.Outlined.FavoriteBorder,
contentDescription = "Favorite",
tint = if (isFavorite) Color.Red else Color.Gray
)
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Accessibility in Jetpack Compose (Android Developers)
- Make apps more accessible — Android Guide (Android Developers)
- Accessibility Scanner (Google Play)
- WCAG 2.1 — Contrast Guidelines (W3C)
- Material Design — Accessibility (Material Design)