Compose semantics and accessibility
Visual pixels are not meaning
Compose creates a semantics tree alongside UI. TalkBack, Switch Access, autofill, and Compose tests consume meaning such as role, label, state, and actions.
@Composable
fun FavoriteToggle(favorite: Boolean, onChange: (Boolean) -> Unit) {
IconToggleButton(
checked = favorite,
onCheckedChange = onChange,
modifier = Modifier.semantics {
stateDescription = if (favorite) "Saved" else "Not saved"
},
) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = null, // parent control already owns the meaning
)
}
}
Adding the same description to both parent and icon can cause duplicate announcements.
Merge at the interaction boundary
A clickable article row containing title, author, and image should often be one semantic action. clickable commonly merges descendants. A row containing separate Bookmark and Open actions should not hide both actions inside one merged node.
Use clearAndSetSemantics only when replacing descendant meaning deliberately; careless clearing makes content invisible to assistive technology.
Custom controls must expose a complete contract
Modifier
.minimumInteractiveComponentSize()
.toggleable(
value = enabled,
role = Role.Switch,
onValueChange = onEnabledChange,
)
A canvas-drawn switch without role, state, action, focus, and adequate target is only a picture.
Test in layers
- Inspect merged and unmerged semantics trees.
- Assert roles/states/actions in Compose tests.
- Run automated accessibility checks.
- Manually use TalkBack, Switch Access, large font, display scaling, and keyboard navigation.
- Test errors and dynamic announcements, not only the happy path.
composeRule.onNode(
hasRole(Role.Switch) and hasStateDescription("Saved")
).assertIsOn().performClick().assertIsOff()
Test IDs are sometimes useful, but user-visible semantics produce stronger tests and better accessibility simultaneously.
Frequent practical failures
- ❌ Icon-only action says “button, unlabeled.”
- ✅ Parent action has a localized purpose such as “Delete invoice.”
- ❌ Every decorative divider/logo becomes a focus stop.
- ✅ Decorative images use
contentDescription = null. - ❌ Error is only red.
- ✅ Error has text/icon semantics and an understandable recovery action.
- ❌ Custom gesture has no keyboard/accessibility action.
- ✅ Equivalent semantic action is exposed.
Sources
Interview exercises
- Why are merged and unmerged semantics trees different?
- Fix a clickable card that contains an independent favorite button.
- Build a TalkBack test plan for checkout failure.