DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Accessibility Advanced Guide — Custom Actions/Traversal Order/Testing

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) }
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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 }
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 { }
)
Enter fullscreen mode Exit fullscreen mode

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
            }
        )
}
Enter fullscreen mode Exit fullscreen mode

Summary

Feature API
Custom actions CustomAccessibilityAction
Traversal order traversalIndex
Dynamic updates liveRegion
Element merge mergeDescendants
Element hide clearAndSetSemantics
  • customActions provide accessible swipe alternatives
  • traversalIndex controls reading order
  • liveRegion notifies on dynamic changes
  • mergeDescendants creates logical grouping

Ready-Made Android App Templates

8 production-ready Android app templates with Jetpack Compose, MVVM, Hilt, and Material 3.

Browse templatesGumroad

Top comments (0)