Compete in the Jetpack Compose #AndroidDevChallenge for a chance to win one of over 1,000 prizes, including a Google Pixel 5. Learn more

Accessibility in Compose

Apps written in Compose should support accessibility for users with different needs. Accessibility services are used to transform what’s shown on screen to a more fitting format for a user with a specific need. To support accessibility services, apps use APIs in the Android framework to expose semantic information about their UI elements. The Android framework will then inform accessibility services about this semantic information. Each accessibility service can choose how best to describe the app to the user. Android provides several accessibility services, including Talkback and Switch Access.

Semantics

Compose uses semantics properties to pass information to accessibility services. Semantics properties provide information about UI elements that are displayed to the user. Most built-in composables like Text and Button fill these semantics properties with information inferred from the composable and its children. Some modifiers like toggleable and clickable will also set certain semantics properties. However, sometimes the framework needs more information to understand how to describe a UI element to the user.

This document describes various situations in which you need to explicitly add extra information to a composable so it can be correctly described to the Android framework. It also explains how to replace the semantics information completely for a given composable. It assumes a basic understanding of accessibility in Android.

Common use cases

To help people with accessibility needs use your app successfully, your app should follow the best practices described on this page.

Describe visual elements

When you define an Image or Icon composable, there is no automatic way for the Android framework to understand what is being displayed. 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 "share" icon highlighted

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

The contentDescription parameter is used to describe a visual element. You should use a localized string, as this will be communicated to the user.

@Composable
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
fun PostImage(post: Post, modifier: Modifier = Modifier) {
  val image = post.imageThumb ?: imageResource(R.drawable.placeholder_1_1)

  Image(
    bitmap = 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 if the element conveys information that the user will need to perform their task. If not, it’s better to leave the description out.

Set custom content descriptions

There might be situations in which you want to explicitly set a content description. In those situations, you can use the semantics modifier to directly assign a content description to a composable. For example, you might want to draw a custom icon, which means you can’t directly use the Icon composable:

@Composable
fun CustomShareIcon(modifier: Modifier = Modifier) {
  val cd = stringResource(R.string.custom_share_icon_content_description)
  Canvas(modifier.semantics { contentDescription = cd }) {
    /* Draw custom icon here */
  }
}

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. If every single low-level composable in your screen is focused independently, a user will have to interact a lot to move across the screen. If elements are merged too aggressively, users might not understand which elements belong together.

When you apply a clickable modifier to a composable, Compose will automatically merge all elements it contains. This also holds for ListItem; elements within a list item will be merged and accessibility services will 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 tell Compose to merge these elements by using the mergeDescendants parameter in the semantics modifier. This way, accessibility services will 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 will 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 will first select 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 would be to define a custom action that allows a user to bookmark the item. Keep in mind that you will also have to explicitly remove the behavior of the bookmark icon itself, to make sure it will not be selected by the accessibility service. This is done with the clearAndSetSemantics modifier:

@Composable
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 is used by the Android framework 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 are used by Compose. 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 will have a hard time navigating such a screen. To aid navigation, you can indicate which elements are headings. In the example above, 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.h5,
    modifier = Modifier.semantics { heading() }
  )
}

Creating custom low-level composables

A more advanced use case involves replacing certain Material components in your app with custom versions. In this scenario, it is vital that you keep accessibility considerations in mind. Say you’re replacing the Material Checkbox with your own implementation. It would be really easy to forget to add the triStateToggleable modifier, which handles the accessibility properties for this component.

As a rule of thumb, you should 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. Make sure to test your custom component implementation with multiple accessibility services to verify its behavior.