Accessibility Snapshots¶
The AccessibilityRenderExtension allows accessibility properties to be visually checked alongside a snapshot of the UI under test. Like regular Paparazzi tests, tests using the AccessibilityRenderExtension provide a way to compare changes that update accessibility handling to “golden snapshots” that have been recorded previously. This can help catch regressions to accessibility support. The AccessibilityRenderExtension does not inform developers whether or not the accessibility properties and their content are appropriate for the specific use case of your UI. That is up to the developer writing the test to understand within the context of the UI under test (see below for some tips on how to verify this).
Set Up¶
To create an accessibility snapshot test, add the AccessibilityRenderExtension to the renderExtensions set in your Paparazzi configuration:
@get:Rule
val paparazzi = Paparazzi(
renderExtensions = setOf(AccessibilityRenderExtension()),
)
Recording and verifying accessibility snapshot tests works identically to regular Paparazzi tests — use ./gradlew recordPaparazzi and ./gradlew verifyPaparazzi.
Both Android Views and Jetpack Compose content are supported. You can snapshot views, composables, or mixed hierarchies:
// Compose
@Test
fun composableTest() {
paparazzi.snapshot {
MyComposable()
}
}
// Android View
@Test
fun viewTest() {
val view = MyCustomView(paparazzi.context)
paparazzi.snapshot(view)
}
// Mixed: View containing ComposeView
@Test
fun mixedTest() {
val mixedView = MixedView(paparazzi.context)
paparazzi.snapshot(mixedView)
}
Limitations¶
RenderingMode.SHRINKis not supported. UsingAccessibilityRenderExtensionwithRenderingMode.SHRINKthrows anIllegalStateException. See #1350 for context.validateAccessibilityis deprecated. The oldervalidateAccessibility: Booleanparameter onPaparazziused Android’sLayoutValidatorfor contrast checking. It has been replaced byAccessibilityRenderExtensionand cannot be used simultaneously with render extensions.
Interpreting Snapshots¶

Figure A: Example accessibility snapshot
Accessibility snapshots render as a split view: the original UI on the left, and a color-coded legend on the right.
- Legend order matches the order a screen reader (TalkBack) will surface elements to the user. In most cases, this should go from start to end, top to bottom.
- Color coding maps each legend entry to the corresponding UI element via colored rectangles overlaid on the left pane and matching color badges in the legend.
- Each highlighted area represents a single screen reader-focusable element.
- Color assignment is deterministic — colors are derived from a hash of the element’s accessibility text, so they remain stable across test runs regardless of view hierarchy order.
- Font scaling independence — the legend text does not scale with the device’s
fontScalesetting, ensuring consistent snapshot output even when testing with large font sizes.
Snapshot labels¶
Each legend entry is a comma-separated string of the accessibility properties for that element. The labels use angle-bracket prefixes to distinguish property types:
| Label | Meaning |
|---|---|
| Plain text | Content description or text content |
<selected> / <unselected> |
Selection state |
<disabled> |
Element is not interactive |
<heading> |
Screen reader heading landmark |
<toggleable>: checked / not checked / indeterminate |
Toggle state (checkbox, switch) |
<on-click>: label |
Click action description (Compose only) |
<progress>: N% or <progress>: indeterminate |
Progress bar value |
<set-progress>: label or <adjustable> |
Slider/adjustable control |
<editable> |
Editable text field |
<live-region>: assertive / polite |
Dynamic content region |
<custom-action>: label |
Custom accessibility action |
<url-action>: text |
URL link annotation |
<click-action>: text |
Clickable link annotation |
<in-list> |
Element is inside a list/collection |
| State description text | Custom state description (replaces selected state when set) |
Role name (e.g., Button, Checkbox, Image) |
Semantic role (Compose only) |
| Error description text | Validation error message |
Supported Properties¶
Content description¶
The main text read by TalkBack to describe a UI element.
- Views:
contentDescriptionoriterableTextForAccessibility(text content ofTextView, etc.) - Compose:
SemanticsProperties.ContentDescription,SemanticsProperties.Text, orSemanticsProperties.EditableText
For merged semantics nodes (e.g., a Row with mergeDescendants = true), child node text is collected and joined with commas.
Role (Compose only)¶
The semantic role of an element — Button, Checkbox, Image, Switch, Tab, RadioButton, etc. Helps assistive technologies identify the purpose of a UI component.
Box(modifier = Modifier.semantics { role = Role.Button }) {
Text("Submit")
}
Selected state¶
Indicates whether an item is currently selected. Renders as <selected> or <unselected>.
Note: If a stateDescription is set, the selected state is suppressed, because TalkBack only reads one or the other.
Disabled state¶
Renders as <disabled> for non-interactive elements. When an element is disabled, click labels are also suppressed (matching TalkBack behavior).
- Views:
isEnabled == false - Compose:
SemanticsProperties.Disabled
On-click label (Compose only)¶
Provides context about what happens when an element is activated. Renders as <on-click>: label.
Box(modifier = Modifier.clickable(onClickLabel = "Add to cart") { }) {
Text("Product")
}
Heading¶
Marks an element as a heading landmark for screen reader navigation. Renders as <heading>.
- Views:
isAccessibilityHeading = true(API 28+) - Compose:
SemanticsProperties.Heading
Toggleable state¶
For checkboxes, switches, and other toggleable elements. Renders as <toggleable>: checked, <toggleable>: not checked, or <toggleable>: indeterminate.
- Views: Detected on
Checkableviews (e.g.,CheckBox,Switch) - Compose:
SemanticsProperties.ToggleableState
State description¶
A custom state description (e.g., “On”/”Off” for a switch). When set, this replaces the default selected state in the accessibility output.
- Views:
stateDescription(API 30+) - Compose:
SemanticsProperties.StateDescription
Error description¶
Provides context about validation errors on form fields. Rendered as the error text itself.
- Compose:
SemanticsProperties.Error
Progress¶
Shows progress bar values. Renders as <progress>: N% for determinate progress or <progress>: indeterminate.
For adjustable controls (sliders), the SetProgress action is also captured:
- With a label:
<set-progress>: label - Without a label:
<adjustable>
// Determinate progress
CircularProgressIndicator(progress = 0.75f)
// Adjustable slider with custom label
Slider(
modifier = Modifier.semantics {
setProgress("Adjust volume") { true }
},
value = 0.5f,
onValueChange = {}
)
Custom actions¶
Additional interactive actions defined on UI elements. Each action renders as <custom-action>: label.
Compose:
Box(modifier = Modifier.semantics {
customActions = listOf(
CustomAccessibilityAction("Delete") { true },
CustomAccessibilityAction("Archive") { true }
)
}) { /* ... */ }
Views (via AccessibilityDelegateCompat):
ViewCompat.setAccessibilityDelegate(button, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.addAction(
AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfoCompat.ACTION_CLICK,
"Custom Click Action"
)
)
}
})
Link annotations (Compose only)¶
Links within AnnotatedString are surfaced as actions:
LinkAnnotation.Url→<url-action>: link textLinkAnnotation.Clickable→<click-action>: link text
val annotatedString = buildAnnotatedString {
append("Visit ")
pushLink(LinkAnnotation.Url("https://example.com"))
append("our website")
pop()
}
Text(text = annotatedString)
// Legend: "Visit our website, <url-action>: our website"
Live region¶
A section of UI that updates dynamically. Screen readers announce changes automatically. Renders as <live-region>: assertive or <live-region>: polite.
- Compose:
SemanticsProperties.LiveRegionwithLiveRegionMode.AssertiveorLiveRegionMode.Polite - Views:
accessibilityLiveRegionwithACCESSIBILITY_LIVE_REGION_ASSERTIVEorACCESSIBILITY_LIVE_REGION_POLITE
Collection info (in-list)¶
Indicates an element is inside a list. Renders as <in-list> at the end of the label. Detected via CollectionInfo on the parent element.
- Compose: Automatically provided by
LazyColumn,LazyRow, etc. - Views: Automatically provided by
ListView,RecyclerView, etc.
Editable¶
Indicates a text field that accepts user input. Renders as <editable>.
- Views: Detected via
AccessibilityNodeInfo.isEditable(e.g.,EditText) - Compose:
SemanticsProperties.IsEditable
Element Visibility & Filtering¶
The extension automatically filters out elements that should not appear in the legend:
View.GONE/View.INVISIBLE: Hidden views and their children are excluded.invisibleToUser()semantics: Compose nodes marked withModifier.semantics { invisibleToUser() }are excluded.alpha(0f): Compose nodes withModifier.alpha(0f)are excluded, matching TalkBack behavior.clearAndSetSemantics: When used, child semantics are replaced by the parent’s overridden semantics. Only the parent’s content appears in the legend.- Empty accessibility text: Elements with no meaningful accessibility text are excluded.
Column {
// Excluded: invisible to user
Text(modifier = Modifier.semantics { invisibleToUser() }, text = "Hidden")
// Excluded: zero alpha
Text(modifier = Modifier.alpha(0f), text = "Transparent")
// Included: overridden semantics
Column(modifier = Modifier.clearAndSetSemantics {
contentDescription = "Custom description"
}) {
Text("Child text is not in legend")
}
}
Traversal Order¶
The legend order reflects the screen reader traversal order, which may differ from visual layout order.
Compose: traversalIndex¶
Lower traversalIndex values are traversed first. The default is 0f. Elements with the same index use layout order as a tiebreaker.
Column {
Text("Third", modifier = Modifier.semantics { traversalIndex = 2f })
Text("First", modifier = Modifier.semantics { traversalIndex = -1f })
Text("Second") // Default traversalIndex = 0f
}
// Legend order: First, Second, Third
IsTraversalGroup groups child elements together for ordering purposes — the group’s traversalIndex positions all its children relative to siblings outside the group.
Views: accessibilityTraversalBefore / accessibilityTraversalAfter¶
These attributes control traversal order via a dependency graph that is resolved with topological sort.
val first = TextView(context).apply {
id = View.generateViewId()
text = "First"
}
val second = TextView(context).apply {
id = View.generateViewId()
text = "Second"
accessibilityTraversalAfter = first.id
}
val third = TextView(context).apply {
id = View.generateViewId()
text = "Third"
accessibilityTraversalAfter = second.id
}
Cycle detection: If traversal constraints form a cycle (e.g., A → B → C → A), the extension detects it and falls back to layout order.
Multi-Window Support¶
The extension handles UI that renders in separate windows. Any window displayed via WindowManager is supported, including:
DropdownMenu(Material3)ModalBottomSheet(Material3)- Dialogs and popups
Accessibility properties from these windows appear in the legend alongside the main content.
paparazzi.snapshot {
Box(Modifier.fillMaxSize()) {
DropdownMenu(expanded = true, onDismissRequest = {}) {
DropdownMenuItem(text = { Text("Option 1") }, onClick = {})
DropdownMenuItem(text = { Text("Option 2") }, onClick = {})
}
}
}
Things to Look For¶
- All visually available text
- All text you see in the UI in the left pane should also be available in the legend on the right.
- Ordering
- The order of the items in the legend should make logical sense within the context of your application.
- Visual cues
- Are there things represented by colors or position that aren’t communicated in the legend? Examples could be red used to represent an error without textual description or a label and value being next to each other visually, but displayed separately in the legend, losing that relationship.
- Image descriptions
- Images and icons that convey meaning should have descriptions for them represented in the legend. On the other hand, images and icons that don’t convey additional meaning (Like a pencil icon paired with visual “Edit” text on a button
[pencil Edit]) shouldn’t have representation in the legend, as that would be repetitive.
- Images and icons that convey meaning should have descriptions for them represented in the legend. On the other hand, images and icons that don’t convey additional meaning (Like a pencil icon paired with visual “Edit” text on a button
- Roles and states
- The correct role or state (header, button, disabled, checked, etc.) should be represented in the legend.
- List context
- Items inside lists should show
<in-list>to indicate they are part of a collection.
- Items inside lists should show
- Live regions
- Dynamically updated content sections should be marked with the appropriate live region mode.
- Custom actions
- Interactive elements with non-standard actions should have those actions represented in the legend.
To help understand whether the accessibility property values you are seeing in your snapshots are sufficient, referencing the WCAG criteria is a good place to start. For example, 4.1.2 Name, Role, Value is a criteria that must be met for any custom component you create (standard Android components will meet this criteria by default). To pass that criteria, the name (e.g. “Submit”), role (e.g. “Button”) and value (if applicable, e.g. “Selected”), must be available to assistive technology users.