A Compose screen can look correct while remaining incomprehensible to assistive technology. Screen readers, Switch Access, and Compose UI tests do not operate on pixels. They use the semantics tree generated alongside the composition tree. That tree describes an element’s name, role, state, and available actions, and it determines what automated tests can find.

Accessibility is therefore more than adding a few contentDescription values before release. Treat semantics as part of a component’s public interface. Its visual implementation may change, but the meaning that users and tests depend on should remain stable.

Prefer components with built-in semantics

Button, Checkbox, Switch, and RadioButton already communicate roles, state, and interactions. Recreating a button with a clickable Box can lose role information, target sizing, keyboard behavior, or disabled state.

When an icon is decorative, prevent a duplicate announcement:

Button(onClick = onSave) {
    Icon(
        imageVector = Icons.Default.Save,
        contentDescription = null,
    )
    Text("Save")
}

The text already names the action, so the icon adds no meaning. A standalone icon button does need a concise action-oriented label such as “Delete draft,” rather than a visual description such as “Trash can icon.”

Give a custom control a name, role, and state

A custom difficulty card should tell assistive technology that it is selectable and whether it is selected. Exposing several internal Text nodes is not enough:

Row(
    modifier = Modifier
        .selectable(
            selected = selected,
            onClick = onSelect,
            role = Role.RadioButton,
        )
        .semantics {
            contentDescription = label
            stateDescription = if (selected) "Selected" else "Not selected"
        }
) {
    DifficultyArtwork()
    Text(label)
}

Production state descriptions should come from string resources for localization. Prefer high-level modifiers such as selectable, toggleable, and clickable when they match the behavior. They add both actions and related semantics, reducing the risk of a control that can be announced but not activated.

Semantic granularity determines navigation cost

A card with a title, description, date, and action can create too many focus stops. semantics(mergeDescendants = true) combines children that logically form one action, and a clickable parent normally merges descendants as well.

More merging is not automatically better. If a card also contains a favorite button, merging the entire subtree can hide that independent action. Keeping the primary card and secondary action as two meaningful focus targets is often clearer than reading everything as one oversized node. clearAndSetSemantics discards descendant information and should be reserved for intentionally replacing the semantics completely.

Print the semantics tree while debugging:

composeTestRule.onRoot().printToLog("Semantics")

Tests use the merged tree by default. When a node cannot be found, inspect the unmerged tree to understand what the component exposes before adding a testTag that exists only for test implementation details.

Do not encode state with color alone

Errors, selection, success, and disabled state need text, shape, iconography, or readable state in addition to color. High contrast alone does not help a person who cannot distinguish the meaning assigned to two colors. Dynamic changes should update stateDescription. A live region can announce an important result, but excessive announcements create a different barrier.

Also test font scaling, touch targets, portrait and landscape layouts, and larger windows. Fixed heights and single-line labels often clip at large text sizes. An operation that depends on a precise gesture needs an accessible alternative action.

Test observable semantics

Compose tests locate nodes through semantics. Assertions about role, state, and action survive refactoring better than assertions about internal layout structure:

composeTestRule
    .onNode(
        hasText("Hard") and
            hasRole(Role.RadioButton)
    )
    .assertIsNotSelected()
    .performClick()
    .assertIsSelected()

If a test can identify a control only as “the second Text in the third Row,” assistive technology probably lacks a clear identity for it as well. Custom semantics properties are useful for business values that standard properties cannot represent, such as a date-picker value. They should not expose purely visual implementation such as color, corner radius, or font size.

Automated checks and manual experience are complementary

Starting with Compose 1.8.0, the official accessibility test integration can use the Accessibility Test Framework. After adding ui-test-junit4-accessibility, enable checks on the rule and scan the root:

composeTestRule.enableAccessibilityChecks()
composeTestRule.onRoot().tryPerformAccessibilityChecks()

Automated checks can flag some contrast, touch-target, and traversal-order issues. They cannot decide whether an announcement is understandable or whether a person can complete a workflow. Before release, turn on TalkBack and Switch Access and complete the primary tasks. Test large fonts, display scaling, light and dark themes, and keyboard or D-pad navigation where applicable.

Review semantics as a component contract

Four questions provide a compact review for every interactive component:

  1. Does the spoken name describe purpose rather than appearance?
  2. Does the role match the interaction?
  3. Are selection, toggle, error, and loading states perceivable?
  4. Can the action be completed without relying on visual touch cues?

When semantics are part of the component API, UI tests naturally move toward user-observable behavior. Good Compose accessibility does not mean maintaining an unrelated extra tree. It means making the visual and semantic trees describe the same product: visible, understandable, operable, and reliably testable.