Testing Android Accessibility with TalkBack (For Real)
The fastest way to understand your app's accessibility is to turn on TalkBack, lock your screen brightness to zero or look away, and try to complete a real task — sign in, add an item to a cart, send a message — using only the screen reader. I do this on every feature I ship, and it's humbling every single time. Code review tells you what you intended to expose; TalkBack tells you what a blind user actually receives: the reading order, the announcements, the dead ends where focus vanishes into an unlabeled icon. Automated scanners help, but nothing replaces the two-minute experience of navigating your own UI without looking at it.
Here's how I test accessibility in a way that finds the problems that matter, and how I keep regressions from creeping back.
Learn the three gestures, then actually use them
You only need a handful of TalkBack gestures to test effectively:
- Swipe right / left — move focus to the next / previous element in reading order.
- Double-tap — activate the focused element.
- Two-finger swipe — scroll.
- Swipe up-then-right — open the global context menu (rarely needed for testing).
The revelation is always the reading order. TalkBack walks the UI roughly top-to-bottom, left-to-right, and if your layout puts a floating action button before the content in the semantics tree, that's what gets read first. Swiping through a screen sequentially exposes ordering bugs that are invisible when you can see everything at once.
The four bugs I find every time
After doing this for years, the same issues recur:
- Icon-only buttons with no label. The back arrow, the overflow menu, the favorite heart — TalkBack announces "Button" with no idea what it does. Every actionable icon needs a
contentDescription. - Decorative images that get announced. A background flourish read aloud as "image" is noise. These should be
nullcontent description so TalkBack skips them. - State not announced. A toggle that looks "on" visually but reads the same whether on or off. It needs
stateDescriptionor the right role so TalkBack says "on"/"off". - Composite items read as fragments. A list row with an avatar, name, timestamp, and preview text that requires four swipes and reads as four disconnected pieces, instead of one coherent "Message from Alex, 2 minutes ago."
Fixing it in Compose semantics
Compose gives you a lot for free — Text is announced, clickable becomes a button with a role — but the four bugs above need explicit semantics.
Label an icon button, mark decoration as skippable:
IconButton(onClick = onBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Navigate up")
}
Icon(painterResource(R.drawable.bg_swoosh), contentDescription = null) // decorative
Announce state:
Switch(
checked = notificationsOn,
onCheckedChange = onToggle,
modifier = Modifier.semantics {
stateDescription = if (notificationsOn) "Notifications on" else "Notifications off"
},
)
Merge a composite row into one focusable node:
Row(
modifier = Modifier
.clickable(onClick = onOpen)
.semantics(mergeDescendants = true) {
contentDescription = "Message from ${msg.sender}, ${msg.relativeTime}"
}
) {
Avatar(msg.sender); Column { Text(msg.sender); Text(msg.preview) }
}
mergeDescendants = true collapses the children into a single swipe stop and lets you author one clean announcement instead of a stuttering sequence. Use clearAndSetSemantics when you want to replace the auto-derived description entirely — handy for a custom-drawn chart where the raw children are meaningless.
Mark headings and group meaningfully
Screen-reader users navigate by headings the way sighted users scan visually. Mark your section titles:
Text("Payment methods", modifier = Modifier.semantics { heading() })
Now TalkBack users can jump heading-to-heading instead of swiping through every element. This is one of the highest-value, lowest-effort fixes, and almost nobody does it. It's especially important on dense screens like settings or a checkout, and it pairs with getting localization and RTL right so the reading order stays correct in right-to-left languages too.
Automate the mechanical checks
Manual testing finds the meaningful problems; automation catches the mechanical regressions so they don't come back. Two layers:
Accessibility Scanner (a Google app you install on the device): sweep a screen and it flags low contrast, touch targets under 48dp, and missing labels with a visual overlay. Great for a quick manual audit.
Automated checks in tests: enable accessibility assertions in your Espresso/Compose UI tests so CI fails on regressions:
// Espresso
AccessibilityChecks.enable().setRunChecksFromRootView(true)
For Compose, assert semantics directly:
composeTestRule.onNodeWithContentDescription("Navigate up").assertHasClickAction()
composeTestRule.onNode(hasText("Payment methods") and isHeading()).assertExists()
These catch "someone shipped an unlabeled button" mechanically. What they can't catch is whether your reading order is sane or your announcements are meaningful — that judgment is still yours, made with TalkBack on.
The habit that actually moves the needle
Automated checks are a floor. The practice that raises the ceiling is the boring one: before you call a feature done, enable TalkBack and complete the core task without looking. If you can't, neither can your users who rely on it. It takes two minutes, it finds real bugs every time, and it turns accessibility from a checkbox into something you've actually experienced.
Ship accessibility fixes in the same PR as the feature — retrofitting TalkBack support after launch costs 3× and still misses stateful interactions tested only visually.
Resources
- Test your app's accessibility (Android)
- Get started with TalkBack
- Accessibility in Compose
- Accessibility Scanner
- WCAG 2.2 quick reference
Frequently asked questions
How do I test my app with TalkBack?
Enable TalkBack in Settings > Accessibility, then put your screen away or close your eyes and try to complete a real task using only swipe-to-navigate and double-tap-to-activate gestures. The point is to experience the linear reading order and hear what each element announces. Reading the code tells you what you intended; navigating with TalkBack tells you what a blind user actually gets.
How do I add accessibility semantics in Jetpack Compose?
Use the semantics and clearAndSetSemantics modifiers plus properties like contentDescription, stateDescription, and heading to describe elements to the accessibility service. Compose derives a lot automatically from Text and clickable, but custom drawing, icon-only buttons, and composite components usually need explicit semantics. Merge related nodes with Modifier.semantics(mergeDescendants = true) so a card reads as one item instead of five separate swipes.
Can accessibility testing be automated on Android?
Partly. The Accessibility Scanner app and the Espresso/Compose accessibility checks catch mechanical issues like small touch targets, low contrast, and missing labels, and you should run them in CI. But automation can't judge whether your reading order makes sense or whether an announcement is meaningful, so automated checks are a floor, not a substitute for navigating the app with TalkBack yourself.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →