Accessibility in Jetpack Compose: Content Descriptions Done Right

Quick answer: Give an
IconorImagea localizedcontentDescriptionwhen its meaning is needed to use or understand the screen. Passnullwhen the graphic is decorative or its meaning is already conveyed by nearby merged text. Describe purpose or result—not pixels—and do not add “button,” “image,” or “icon” when Compose semantics already announces the role.
contentDescription is not generic alt text for every Compose element. It is semantic information for visual graphics that Android cannot infer from pixels. The Compose accessibility defaults make this distinction explicit: Icon and Image need a textual description when meaningful, while Material controls, text, and behaviors such as clickable bring their own semantics.
Decide whether the graphic has semantic meaning
Before writing a string, ask one question: if a screen-reader user never heard about this graphic, would they miss information or be unable to complete an action?
| Graphic or context | contentDescription | Why |
|---|---|---|
Standalone share IconButton | "Share article" | The icon is the control’s only visible label. |
| Back arrow in a top app bar | "Navigate up" | The action is essential and the arrow alone is ambiguous. |
| Button with an icon and visible Save text | null on the icon | The text and parent button already provide the meaning. |
| Divider flourish, background texture, redundant brand mark | null | Announcing it adds noise without information. |
| Product photo with no equivalent visible text | A concise product identity | The image carries information the user otherwise misses. |
| Product photo beside the identical product title | Often null | Repeating the title can make list navigation tedious. |
Passing null is intentional: it tells accessibility services that a visual element has no independent action or state to announce. An empty or vague string is not a substitute. Android’s accessible-composables guidance likewise recommends null for purely decorative graphics.
Describe an icon-only action by its result
An icon-only button needs a description because the icon is its only label. Use a localized resource and name what the action does from the user’s perspective.
@Composable
fun ShareArticleButton(
onShare: () -> Unit,
) {
IconButton(onClick = onShare) {
Icon(
imageVector = Icons.Outlined.Share,
contentDescription = stringResource(R.string.share_article),
)
}
}Good descriptions are specific enough to distinguish neighboring controls:
| Prefer | Avoid | Reason |
|---|---|---|
"Share article" | "Share icon" | Describes the result, not the drawing. |
"Remove Draft: Travel plan" | "Remove" in every row | Gives repeated list actions unique context. |
"Navigate up" | "Arrow back button" | The control role is already announced. |
"Show password" | "Eye" | Explains the state-changing action. |
Do not include words such as “button,” “image,” or “icon” unless the role itself is genuinely unclear. A screen reader can announce the button role through its semantics; adding it again produces repetitive speech. Android’s general accessibility guidance specifically recommends descriptions that convey purpose and interaction result rather than visual details, and advises making repeated descriptions unique.
Mark decorative graphics as decorative
An image that only improves appearance should not create another TalkBack stop. Pass null directly to Icon or Image.
@Composable
fun EmptyStateHeader() {
Image(
painter = painterResource(R.drawable.empty_inbox_art),
contentDescription = null,
)
Text("Your inbox is empty")
}The illustration adds tone, while the text gives the user the actual information. Announcing a generic description such as “empty inbox illustration” makes navigation longer without helping the task. If the image later communicates a status, instructions, or unique data that the text does not cover, revisit the decision and provide a concise description.
Do not label an icon twice
Icons inside a text-labelled Material button are usually decorative. The parent control merges its descendants, and the visible text supplies the accessible name.
Button(onClick = onSave) {
Icon(
imageVector = Icons.Outlined.Save,
contentDescription = null,
)
Spacer(Modifier.width(ButtonDefaults.IconSpacing))
Text("Save")
}Giving the icon "Save" here can cause an unnecessarily repetitive announcement such as “Save, Save, button.” The same rule applies to a chevron beside a labelled row, a status glyph that duplicates text, and a thumbnail beside an identical title. Check the merged semantics tree when in doubt; Find Nodes and Assert UI with Compose Test APIs shows how to inspect it.
Make informational images concise and contextual
Use a description when an image itself communicates required information. Keep it brief and task-oriented rather than attempting to narrate every pixel.
@Composable
fun WeatherAlert(
alert: WeatherAlertUi,
) {
Image(
painter = painterResource(alert.illustrationRes),
contentDescription = stringResource(
R.string.weather_alert_image_description,
alert.conditionName,
),
)
Text(alert.headline)
}This is appropriate only if the illustration adds information not already supplied by headline. If a text alert already says everything the user needs, the image is decorative and should use null. For user-generated or remote images, prefer a meaningful author-supplied description when one is available; do not invent detailed claims about an image that the app cannot verify.
Use string resources for every description. They are user-facing content, need localization and review, and often contain dynamic context such as a message subject, item name, or current state. Avoid building these strings with hardcoded concatenation because it makes translation and grammar harder.
Content descriptions are not the only action semantics
For a custom clickable surface, an icon description alone is rarely the right answer. Let visible text identify the item, use clickable for the action, and add an onClickLabel when the action’s outcome needs a clearer hint.
Row(
modifier = Modifier
.clickable(
onClickLabel = stringResource(R.string.open_article),
role = Role.Button,
onClick = onOpen,
)
.padding(16.dp),
) {
Icon(
imageVector = Icons.Outlined.Article,
contentDescription = null,
)
Text(article.title)
}The row’s text gives the item identity; the click label tells a screen reader what activation does. Use standard Material components first whenever they fit because they already include interaction semantics. For custom gesture and click behavior, see Click, Long Press, Drag, Swipe, and Gesture Detection in Compose.
The next layer of accessibility—roles, state, toggle values, custom actions, and grouping—belongs in the semantics tree. A content description cannot repair a control that has the wrong role or no accessible action.
Test the semantics and listen to the result
Compose UI tests can verify that the description and action exist:
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
composeTestRule
.onNodeWithContentDescription("Share article")
.assertHasClickAction()Use the same localized resource in production and tests rather than hardcoding an English string in one place. This kind of test catches a missing or changed semantic. It cannot tell you whether the wording is useful, non-redundant, or understandable in context.
Run TalkBack on a device or emulator and navigate the actual screen in focus order. Listen for duplicate labels, vague actions, and visual details that do not help the task. Accessibility Scanner can identify some mechanical issues, but manual assistive-technology testing is still necessary. The official Compose accessibility codelab recommends manual TalkBack testing alongside automated checks.
Common mistakes
Giving every image a description
This turns decorative art, repeated thumbnails, and visual separators into unnecessary focus stops. Describe only graphics with independent meaning.
Describing the shape instead of the outcome
“Three dots,” “blue circle,” or “arrow” may be technically accurate but do not explain the result of activation. Prefer “More options,” “Show details,” or “Navigate up” when those are the real purposes.
Repeating visible text
An icon inside a Save button does not need another Save description. Let the parent control merge the meaningful content and make the child icon decorative.
Using contentDescription to repair a custom control
A description does not create a click action, button role, toggle state, focus behavior, or keyboard path. Use the appropriate Compose component or semantic modifier for the behavior itself.
Content-description checklist
- Describe meaningful standalone graphics and icon-only actions with localized, task-oriented text.
- Pass
nullfor decorative or duplicate graphics. - Let visible button or row text speak for an accompanying icon whenever their semantics merge.
- Give repeated actions context that identifies the affected item.
- Avoid “button,” “image,” and icon-shape descriptions when role and purpose are already exposed.
- Test for semantic presence, then use TalkBack to judge clarity in the real screen.
Thoughtful descriptions make the screen faster to navigate, not more verbose. That is the standard: say the useful thing once, at the point where a user needs it.