Key steps to improve Compose accessibility

To help people with accessibility needs use your app successfully, design your app to support key accessibility requirements.

Consider minimum touch target sizes

Any on-screen element that someone can click, touch, or interact with should be large enough for reliable interaction. When sizing these elements, make sure to set the minimum size to 48dp to correctly follow the Material Design accessibility guidelines.

Material components—like Checkbox, RadioButton, Switch, Slider, and Surface—set this minimum size internally, but only when the component can receive user actions. For example, when a Checkbox has its onCheckedChange parameter set to a non-null value, the checkbox includes padding to have a width and height of at least 48 dp.

@Composable
private fun CheckableCheckbox() {
    Checkbox(checked = true, onCheckedChange = {})
}

When the onCheckedChange parameter is set to null, the padding is not included, because the component cannot be interacted with directly.

@Composable
private fun NonClickableCheckbox() {
    Checkbox(checked = true, onCheckedChange = null)
}

Figure 1. A checkbox without padding.

When implementing selection controls like Switch, RadioButton, or Checkbox, you typically lift the clickable behavior to a parent container, set the click callback on the composable to null, and add a toggleable or selectable modifier to the parent composable.

@Composable
private fun CheckableRow() {
    MaterialTheme {
        var checked by remember { mutableStateOf(false) }
        Row(
            Modifier
                .toggleable(
                    value = checked,
                    role = Role.Checkbox,
                    onValueChange = { checked = !checked }
                )
                .padding(16.dp)
                .fillMaxWidth()
        ) {
            Text("Option", Modifier.weight(1f))
            Checkbox(checked = checked, onCheckedChange = null)
        }
    }
}

When the size of a clickable composable is smaller than the minimum touch target size, Compose still increases the touch target size. It does so by expanding the touch target size outside of the boundaries of the composable.

The following example contains a very small clickable Box. The touch target area is automatically expanded beyond the boundaries of the Box, so tapping next to the Box still triggers the click event.

@Composable
private fun SmallBox() {
    var clicked by remember { mutableStateOf(false) }
    Box(
        Modifier
            .size(100.dp)
            .background(if (clicked) Color.DarkGray else Color.LightGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .clickable { clicked = !clicked }
                .background(Color.Black)
                .size(1.dp)
        )
    }
}

To prevent possible overlap between touch areas of different composables, always use a large enough minimum size for the composable. In the example, that would mean using the sizeIn modifier to set the minimum size for the inner box:

@Composable
private fun LargeBox() {
    var clicked by remember { mutableStateOf(false) }
    Box(
        Modifier
            .size(100.dp)
            .background(if (clicked) Color.DarkGray else Color.LightGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .clickable { clicked = !clicked }
                .background(Color.Black)
                .sizeIn(minWidth = 48.dp, minHeight = 48.dp)
        )
    }
}

Add click labels

You can use a click label to add semantic meaning to the click behavior of a composable. Click labels describe what happens when the user interacts with the composable. Accessibility services use click labels to help describe the app to users with specific needs.

Set the click label by passing a parameter in the clickable modifier:

@Composable
private fun ArticleListItem(openArticle: () -> Unit) {
    Row(
        Modifier.clickable(
            // R.string.action_read_article = "read article"
            onClickLabel = stringResource(R.string.action_read_article),
            onClick = openArticle
        )
    ) {
        // ..
    }
}

Alternatively, if you don't have access to the clickable modifier, set the click label in the semantics modifier:

@Composable
private fun LowLevelClickLabel(openArticle: () -> Boolean) {
    // R.string.action_read_article = "read article"
    val readArticleLabel = stringResource(R.string.action_read_article)
    Canvas(
        Modifier.semantics {
            onClick(label = readArticleLabel, action = openArticle)
        }
    ) {
        // ..
    }
}

Describe visual elements

When you define an Image or Icon composable, there is no automatic way for the Android framework to understand what the app is displaying. You need to pass a textual description of the visual element.

Imagine a screen where the user can share the current page with friends. This screen contains a clickable share icon:

A strip of clickable icons, with the

Based on the icon alone, the Android framework can't describe it to a visually impaired user. The Android framework needs an additional textual description of the icon.

The contentDescription parameter describes a visual element. Use a localized string, as it is visible to the user.

@Composable
private fun ShareButton(onClick: () -> Unit) {
    IconButton(onClick = onClick) {
        Icon(
            imageVector = Icons.Filled.Share,
            contentDescription = stringResource(R.string.label_share)
        )
    }
}

Some visual elements are purely decorative and you might not want to communicate them to the user. When you set the contentDescription parameter to null, you indicate to the Android framework that this element does not have associated actions or state.

@Composable
private fun PostImage(post: Post, modifier: Modifier = Modifier) {
    val image = post.imageThumb ?: painterResource(R.drawable.placeholder_1_1)

    Image(
        painter = image,
        // Specify that this image has no semantic meaning
        contentDescription = null,
        modifier = modifier
            .size(40.dp, 40.dp)
            .clip(MaterialTheme.shapes.small)
    )
}

It is up to you to decide whether a given visual element needs a contentDescription. Ask yourself whether the element conveys information that the user will need to perform their task. If not, it's better to leave the description out.

Merge elements

Accessibility services like Talkback and Switch Access allow users to move focus across elements on the screen. It is important that elements are focused at the right granularity. When every single low-level composable in your screen is focused independently, users have to interact a lot to move across the screen. If elements merge together too aggressively, users might not understand which elements belong together

When you apply a clickable modifier to a composable, Compose automatically merges all elements the composable contains. This also holds for ListItem; elements within a list item merge together, and accessibility services view them as one element.

It is possible to have a set of composables that form a logical group, but that group is not clickable or part of a list item. You'd still want accessibility services to view them as one element. For example, imagine a composable that shows a user's avatar, their name, and some extra information:

A group of UI elements including a user's name. The name is selected.

You can enable Compose to merge these elements by using the mergeDescendants parameter in the semantics modifier. This way, accessibility services select only the merged element, and all semantics properties of the descendants are merged.

@Composable
private fun PostMetadata(metadata: Metadata) {
    // Merge elements below for accessibility purposes
    Row(modifier = Modifier.semantics(mergeDescendants = true) {}) {
        Image(
            imageVector = Icons.Filled.AccountCircle,
            contentDescription = null // decorative
        )
        Column {
            Text(metadata.author.name)
            Text("${metadata.date} • ${metadata.readTimeMinutes} min read")
        }
    }
}

Accessibility services now focus on the whole container at once, merging their contents:

A group of UI elements including a user's name. All the elements are selected together.

Add custom actions

Take a look at the following list item:

A typical list item, containing an article title, author, and bookmark icon.

When you use a screen reader like Talkback to hear what's displayed on the screen, it first selects the whole item, and then the bookmark icon.

The list item, with all the elements selected together.

The list item, with just the bookmark icon selected

In a long list, this can become very repetitive. A better approach is to define a custom action that allows a user to bookmark the item. Keep in mind that you also have to explicitly remove the behavior of the bookmark icon itself to make sure it isn't selected by the accessibility service. This is done with the clearAndSetSemantics modifier:

@Composable
private fun PostCardSimple(
    /* ... */
    isFavorite: Boolean,
    onToggleFavorite: () -> Boolean
) {
    val actionLabel = stringResource(
        if (isFavorite) R.string.unfavorite else R.string.favorite
    )
    Row(
        modifier = Modifier
            .clickable(onClick = { /* ... */ })
            .semantics {
                // Set any explicit semantic properties
                customActions = listOf(
                    CustomAccessibilityAction(actionLabel, onToggleFavorite)
                )
            }
    ) {
        /* ... */
        BookmarkButton(
            isBookmarked = isFavorite,
            onClick = onToggleFavorite,
            // Clear any semantics properties set on this node
            modifier = Modifier.clearAndSetSemantics { }
        )
    }
}

Describe an element's state

A composable can define a stateDescription for semantics which the Android framework uses to read out the state that the composable is in. For example, a toggleable composable can be in either a "checked" or an "unchecked" state. In some cases, you might want to override the default state description labels that Compose uses. You can do so by explicitly specifying the state description labels before defining a composable as toggleable:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
    val stateSubscribed = stringResource(R.string.subscribed)
    val stateNotSubscribed = stringResource(R.string.not_subscribed)
    Row(
        modifier = Modifier
            .semantics {
                // Set any explicit semantic properties
                stateDescription = if (selected) stateSubscribed else stateNotSubscribed
            }
            .toggleable(
                value = selected,
                onValueChange = { onToggle() }
            )
    ) {
        /* ... */
    }
}

Define headings

Apps sometimes show a lot of content on one screen in a scrollable container. For example, a screen could show the full contents of an article that the user is reading:

Screenshot of a blog post, with the article text in a scrollable container.

Users with accessibility needs have difficulty navigating such a screen. To aid navigation, indicate which elements are headings. In the preceding example, each subsection title could be defined as a heading for accessibility. Some accessibility services, like Talkback, allow users to navigate directly from heading to heading.

In Compose, you indicate that a composable is a heading by defining its semantics property:

@Composable
private fun Subsection(text: String) {
    Text(
        text = text,
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier.semantics { heading() }
    )
}

Handle custom composables

Whenever you replace certain Material components in your app with custom versions, you must keep accessibility considerations in mind.

Say you're replacing the Material Checkbox with your own implementation. You could forget to add the triStateToggleable modifier, which handles the accessibility properties for this component.

As a rule of thumb, look at the implementation of the component in the Material library and mimic any accessibility behavior that you can find. Additionally, make heavy use of Foundation modifiers, as opposed to UI level modifiers, as these include accessibility considerations out of the box.

Test your custom component implementation with multiple accessibility services to verify its behavior.

Additional resources