What You'll Learn
Advanced accessibility (custom actions, traversal order, live regions, testing, TalkBack support) explained.
Custom Actions
@Composable
fun SwipeableItem(
item: Item,
onDelete: () -> Unit,
onArchive: () -> Unit
) {
Box(
Modifier
.fillMaxWidth()
.semantics {
customActions = listOf(
CustomAccessibilityAction("Delete") { onDelete(); true },
CustomAccessibilityAction("Archive") { onArchive(); true }
)
}
) {
ListItem(
headlineContent = { Text(item.title) },
supportingContent = { Text(item.description) }
)
}
}
Traversal Order
@Composable
fun CustomTraversalOrder() {
val (first, second, third) = remember { FocusRequester.createRefs() }
Column {
// Control focus order differently than visual order
Text(
"Read 3rd",
Modifier.semantics { traversalIndex = 3f }
)
Text(
"Read 1st",
Modifier.semantics { traversalIndex = 1f }
)
Text(
"Read 2nd",
Modifier.semantics { traversalIndex = 2f }
)
}
}
Live Regions (Dynamic Update Notification)
@Composable
fun TimerDisplay(remainingSeconds: Int) {
Text(
text = "$remainingSeconds seconds",
modifier = Modifier.semantics {
liveRegion = LiveRegionMode.Polite // TalkBack reads on change
contentDescription = "Remaining $remainingSeconds seconds"
},
style = MaterialTheme.typography.displayLarge
)
}
@Composable
fun ErrorBanner(message: String?) {
message?.let {
Surface(
color = MaterialTheme.colorScheme.errorContainer,
modifier = Modifier
.fillMaxWidth()
.semantics {
liveRegion = LiveRegionMode.Assertive // Read immediately
}
) {
Text(it, Modifier.padding(16.dp), color = MaterialTheme.colorScheme.onErrorContainer)
}
}
}
Semantics merge/clear
@Composable
fun ClickableCard(title: String, subtitle: String, onClick: () -> Unit) {
Card(
onClick = onClick,
modifier = Modifier.semantics(mergeDescendants = true) {
// Entire card is one element for TalkBack
contentDescription = "$title. $subtitle"
}
) {
Column(Modifier.padding(16.dp)) {
Text(title, style = MaterialTheme.typography.titleMedium)
Text(subtitle, style = MaterialTheme.typography.bodySmall)
}
}
}
// Hide decorative element
Icon(
Icons.Default.ChevronRight,
contentDescription = null, // TalkBack ignores
modifier = Modifier.clearAndSetSemantics { }
)
Accessibility Testing
@Test
fun cardHasCorrectSemantics() {
composeTestRule.setContent {
ClickableCard("Title", "Subtitle", onClick = {})
}
composeTestRule
.onNodeWithContentDescription("Title. Subtitle")
.assertIsDisplayed()
.assertHasClickAction()
}
@Test
fun timerUpdatesLiveRegion() {
composeTestRule.setContent {
TimerDisplay(remainingSeconds = 30)
}
composeTestRule
.onNodeWithContentDescription("Remaining 30 seconds")
.assertIsDisplayed()
}
@Test
fun customActionsAvailable() {
composeTestRule.setContent {
SwipeableItem(testItem, onDelete = {}, onArchive = {})
}
composeTestRule
.onNodeWithText(testItem.title)
.assert(
SemanticsMatcher("has delete action") {
it.config.getOrNull(SemanticsActions.CustomActions)
?.any { action -> action.label == "Delete" } == true
}
)
}
Summary
| Feature | API |
|---|---|
| Custom actions | CustomAccessibilityAction |
| Traversal order | traversalIndex |
| Dynamic updates | liveRegion |
| Element merge | mergeDescendants |
| Element hide | clearAndSetSemantics |
-
customActionsprovide accessible swipe alternatives -
traversalIndexcontrols reading order -
liveRegionnotifies on dynamic changes -
mergeDescendantscreates logical grouping
Ready-Made Android App Templates
8 production-ready Android app templates with Jetpack Compose, MVVM, Hilt, and Material 3.
Browse templates → Gumroad
Top comments (0)