Skip to content
All posts

Accessibility on Android in 2026: a TalkBack and Compose semantics checklist that ships

A practical Android accessibility checklist for 2026 — TalkBack, Compose semantics, touch targets, and the testing pass I run on every screen before release.

MFKAPPS 4 min read

Accessibility on Android usually gets treated as a pre-launch checklist item, squeezed in after the UI is “done.” That ordering is backwards, and it shows: a screen built without semantics in mind takes hours to retrofit, while the same screen built with semantics from the start costs almost nothing extra. This is the checklist I actually run — not a policy document, a list of things I do while writing Compose UI, plus the testing pass before anything ships.

Why retrofitting is expensive and building-in isn’t

TalkBack, Android’s screen reader, doesn’t read pixels — it reads the semantics tree Compose generates alongside your UI. If you never think about that tree while writing a screen, TalkBack ends up reading raw implementation details: an icon-only button announces as “Button,” a decorative image announces as “Image,” and a row of three separate composables announces as three separate stops instead of one sentence. Fixing that after the fact means going back through every composable and asking “what should this actually say,” which is slower than asking the same question the first time.

The fix is to treat semantics as part of the component’s API, not an afterthought bolted onto the Modifier chain at the end.

The checklist

1. Every non-text control has a contentDescription — or none

If a button has visible text, don’t add a redundant description — TalkBack already reads the text, and a duplicate description makes it read twice. If a button is icon-only, the description is mandatory:

IconButton(onClick = { onDelete(item.id) }) {
    Icon(
        imageVector = Icons.Default.Delete,
        contentDescription = stringResource(R.string.delete_item, item.name)
    )
}

Purely decorative images get contentDescription = null explicitly. An image with no description at all still gets announced as “unlabeled image,” which is worse than silence.

A card with a title, a subtitle, and a chip reads as three separate TalkBack stops by default, which means three swipes to hear one row. Wrap the group so it announces as a single unit:

Row(
    modifier = Modifier.semantics(mergeDescendants = true) {}
) {
    Text(title)
    Text(subtitle)
    Badge { Text(status) }
}

Test this by turning on TalkBack and swiping through the screen once. If a single logical row takes more than one swipe, it needs merging.

3. Touch targets are 48dp, not the icon’s visual size

A 24dp icon with no padding is a 24dp touch target, which fails both the accessibility guidelines and basic usability for anyone with reduced dexterity. Modifier.minimumInteractiveComponentSize() handles this in current Compose without manual padding math — apply it to every tappable icon, not just the ones that feel cramped in a review.

4. Don’t communicate state with color alone

A red border on an invalid field is invisible to a colorblind user and unannounced to a TalkBack user. Pair every color-only signal with a text or icon change: an error message under the field, an error icon next to the label. This is also just better UX for a sighted user glancing quickly — color alone requires more attention to parse than a word does.

5. Live regions for content that changes without a click

If a total updates after the user types an amount, TalkBack won’t notice unless the composable is marked as a live region:

Text(
    text = "Total: $formattedTotal",
    modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite }
)

Polite waits for the current announcement to finish; Assertive interrupts. Use Assertive sparingly — an error that needs immediate attention, not a running total.

The testing pass

Code review catches maybe half of what actually matters, because most of these issues are only obvious with the screen reader on. Before I ship a screen, I run three checks:

  1. TalkBack, swiping through the whole screen with the display off, listening only. If I can’t tell what a control does from the announcement alone, it needs a better description.
  2. Accessibility Scanner (the standalone Google app) for touch target size and contrast ratio — it flags both automatically and points at the exact composable.
  3. Font scale at 200% in system settings. Text that truncates, overlaps, or gets clipped at large scale is a bug, not an edge case — a meaningful share of real users run elevated font sizes permanently.

This matters more for some apps than others. OldSchool, the medication reminder app, skews toward an older user base where larger font scales and screen readers aren’t edge cases at all — they’re closer to the median. Building the checklist into the component library once meant I didn’t have to re-derive it screen by screen as the app grew.

The takeaway

Accessibility work that happens at the end of a project is expensive because it’s corrective — someone has to notice the gap, then go back and patch every screen individually. Accessibility work that happens as part of writing each composable is closer to free, because the questions (“what does this announce as,” “is this one stop or three,” “is the touch target real”) take seconds to answer while you’re already looking at the code. Treat the checklist as a habit for new screens, not a release-week fire drill, and it stops being a checklist at all.