Accessibility in Compose

Modern Android Mid-level Kotlin 2.0+ API 24+ Reviewed By Best Latest Apps

Pick the fix

TalkBack reads a news row as five separate items: image, title, author, date, and a heart icon. Best fix?

Options
Hint 1 of 1

How do you combine children into one node?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A Make the row clickable or mergeDescendants and give the heart a clear action label

Correct. One focus stop for the row, with the action described.

Why not the others?
  • B Remove all content descriptions. Loses information.
  • C Add 'button' to every description. Noise, and roles do that.
  • D Disable TalkBack for the list. Excludes users.

30-second answer

Compose builds a semantics tree that TalkBack and other services read. Give icons a contentDescription (or null if decorative), merge related items with Modifier.semantics(mergeDescendants = true) or clickable rows, set roles and state descriptions, mark headings, keep touch targets at least 48dp, use sp for text so it scales, and check contrast. Test with TalkBack and Accessibility Scanner.

Practise saying it · 30 s. No recording, just a timer

Explain Compose accessibility.

Say your answer out loud, then check which of these points you covered:

  • Semantics tree
  • contentDescription
  • Merge and roles
  • 48dp targets, sp text
  • Test with TalkBack

Deep dive

clearAndSetSemantics { } replaces children's semantics with your own.

Modifier.semantics { heading() } lets TalkBack jump between sections.

Material components already set roles and minimum touch sizes.

Android 14 scales fonts up to 200% non-linearly; test large font sizes.

A row that reads as one item Compile-checked · Kotlin 2.4.21
@Composable
fun ArticleRow(title: String, author: String, saved: Boolean, onToggleSave: () -> Unit) {
    Row(
        Modifier
            .semantics(mergeDescendants = true) {
                stateDescription = if (saved) "Saved" else "Not saved"
            }
            .padding(16.dp),
    ) {
        Column(Modifier.weight(1f)) {
            Text(title)
            Text(author)
        }
        IconToggleButton(checked = saved, onCheckedChange = { onToggleSave() }) {
            Icon(Icons.Default.Favorite, contentDescription = "Save article")
        }
    }
}

Common mistakes

  • Content descriptions like 'image' or 'button'.
  • Clickable Text with no role.
  • Fixed-height containers that clip large text.

Senior take & follow-ups

Name semantics first, then concrete modifiers and how you test.

Follow-up questions

Think of your answer first, then open the hint.

  1. When is contentDescription null right?

    Show hint

    When the image is decorative or the text next to it says the same.

  2. Why merge descendants?

    Show hint

    So a row reads as one item instead of many.

Compare on iOS: Supporting VoiceOver Same ideas; SwiftUI uses accessibility modifiers instead of semantics.

Suggest a fix if something here is wrong or out of date.