Android accessibility
Skill GuillemRoca/agent-skills-android/skills/android-accessibility
Production-grade engineering skills for AI coding agents tailored to Android
npx -y skills add GuillemRoca/agent-skills-android --skill android-accessibilityAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when building or reviewing Android UI for accessibility. Covers TalkBack, content descriptions, touch targets, Compose semantics, focus order, and Accessibility Scanner testing.
SKILL.md
11.1 KB, as published. Nobody here has run it
Android Accessibility
Overview
Accessibility is not optional — it's a requirement for reaching all users. This skill covers building accessible Android UIs with Jetpack Compose semantics, proper content descriptions, touch target sizing, TalkBack compatibility, and systematic testing with Accessibility Scanner.
When to Use
- Building new UI screens or components
- Reviewing existing UI for accessibility compliance
- Fixing accessibility issues flagged by Accessibility Scanner or users
- Before shipping any user-facing feature
Skip when: Working on non-UI code (repositories, use cases, networking).
Core Standards
| Requirement | Standard |
|---|---|
| Touch targets | Minimum 48dp x 48dp |
| Color contrast (text) | >= 4.5:1 (normal), >= 3:1 (large 18sp+) |
| Color contrast (UI components) | >= 3:1 against adjacent colors |
| Content descriptions | All meaningful visual elements |
| Focus order | Logical reading order |
| No color-only information | Additional indicators required |
Core Process
Step 1: Content Descriptions
- Every meaningful visual element needs a description:
// Icons that convey information
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete task", // Descriptive, not "delete icon"
)
// Decorative elements — explicitly null
Icon(
imageVector = Icons.Default.Circle,
contentDescription = null, // Decorative, TalkBack skips it
)
// Images
Image(
painter = painterResource(R.drawable.user_avatar),
contentDescription = "Profile photo of ${user.name}",
)
// IconButtons — the button gets the description
IconButton(
onClick = onDelete,
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete task", // On the Icon, read by TalkBack
)
}
- Description rules:
- Describe the action or meaning, not the visual appearance
- "Delete task" not "red trash can icon"
- "Search" not "magnifying glass"
- Use
nullfor purely decorative elements - Include dynamic content: "Profile photo of John" not just "profile photo"
Step 2: Touch Targets
- Enforce minimum 48dp touch targets:
// Compose handles this automatically for clickable elements,
// but verify small elements:
IconButton(
onClick = onToggle,
modifier = Modifier.size(48.dp), // At least 48dp
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Mark complete",
modifier = Modifier.size(24.dp), // Icon can be smaller
)
}
// For custom clickable elements, ensure minimum touch area:
@Composable
fun SmallChip(
label: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Surface(
onClick = onClick,
modifier = modifier.defaultMinSize(minHeight = 48.dp, minWidth = 48.dp),
) {
Text(label, modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp))
}
}
Step 3: Compose Semantics
- Use semantics for rich accessibility information:
// Merge child semantics for a single TalkBack announcement
@Composable
fun TaskItem(
task: Task,
onToggle: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.semantics(mergeDescendants = true) {
// Custom state description
stateDescription = if (task.completed) "Completed" else "Not completed"
// Custom action
customActions = listOf(
CustomAccessibilityAction("Toggle completion") {
onToggle()
true
}
)
}
.clickable(onClick = onToggle),
) {
Checkbox(checked = task.completed, onCheckedChange = null)
Text(task.title)
}
}
// Headings for navigation
Text(
text = "My Tasks",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.semantics { heading() },
)
// Live regions for dynamic content
Text(
text = "3 items remaining",
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
)
- Semantics rules:
mergeDescendants = truefor compound components (card with title + subtitle)heading()for section titles (enables TalkBack heading navigation)liveRegionfor content that updates dynamically (counters, status)stateDescriptionfor custom state (not just "checked/unchecked")Rolefor custom interactive elements
Step 4: Focus Order
- Ensure logical focus order:
// Default: top-to-bottom, start-to-end (matches visual order)
// Override when visual order differs from reading order:
@Composable
fun HeaderWithAction(
title: String,
onAction: () -> Unit,
) {
Row {
// TalkBack reads title first, then action button
Text(
text = title,
modifier = Modifier
.weight(1f)
.semantics { heading() },
)
IconButton(onClick = onAction) {
Icon(Icons.Default.Settings, contentDescription = "Settings")
}
}
}
// Focus management for dialogs and sheets:
@Composable
fun TaskDialog(onDismiss: () -> Unit) {
val focusRequester = remember { FocusRequester() }
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
"Delete Task?",
modifier = Modifier.focusRequester(focusRequester),
)
},
// Request focus when dialog appears
confirmButton = { /* ... */ },
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
Step 5: Color and Contrast
- Don't rely on color alone:
// BAD: only color distinguishes status
Text(
text = task.title,
color = if (task.overdue) Color.Red else Color.Black,
)
// GOOD: color + icon + text
Row {
if (task.overdue) {
Icon(
Icons.Default.Warning,
contentDescription = null, // Read as part of merged semantics
tint = MaterialTheme.colorScheme.error,
)
}
Text(
text = task.title,
color = if (task.overdue) MaterialTheme.colorScheme.error
else MaterialTheme.colorScheme.onSurface,
)
if (task.overdue) {
Text(
text = "Overdue",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.labelSmall,
)
}
}
- Use Material theme colors — they're designed for contrast compliance:
onSurfaceonsurface— guaranteed contrastonPrimaryonprimary— guaranteed contrasterrorandonError— guaranteed contrast
Step 6: Testing
-
TalkBack testing (manual):
- Enable TalkBack: Settings → Accessibility → TalkBack
- Swipe right to move to next element
- Verify announcements make sense without seeing the screen
- Check that all interactive elements are reachable
- Verify no elements are skipped or read in wrong order
-
Accessibility Scanner (automated):
- Install from Play Store: "Accessibility Scanner" by Google
- Run on every screen before shipping
- Fixes: touch target size, contrast ratio, content descriptions
-
android layoutfor scriptable accessibility assertions:android layout --prettyreturns JSON withcontent-desc,role, andboundsper node — turn it into automated scans you can run from a script or CI step.# Fail the build if any Button is missing content-desc android layout --pretty \ | jq -e '.nodes[] | select(.role=="Button" and ((.["content-desc"] // "") == ""))' \ && echo "FAIL: unlabeled button" && exit 1 # Flag touch targets smaller than 48dp (assuming density-converted bounds) android layout --pretty \ | jq '.nodes[] | select(.role=="Button") | {id: .["resource-id"], bounds}' \ | review_target_sizes.shUse
android layout --diffafter an interaction to inspect only what changed — keeps assertions tight when verifying live regions, error toasts, or focus shifts. Whenlayoutreturns nothing useful (WebView, animation), fall back toandroid screen capture --annotate -o screen.pngand visually verify focus order. Seereferences/android-cli-reference.md.No device needed for composables:
android studio render-compose-preview --print-semantics <file> <composable>emits the accessibility semantics tree of a@Previewas JSON — assert oncontentDescription,role, and heading semantics straight from the preview, before anything is installed (requires a running Android Studio with Gemini; seereferences/android-cli-reference.md). -
Compose UI tests for accessibility:
@Test
fun taskItem_hasContentDescription() {
composeTestRule.setContent {
TaskItem(task = Task("1", "Buy groceries", false), onToggle = {})
}
composeTestRule
.onNodeWithContentDescription("Buy groceries")
.assertExists()
}
@Test
fun deleteButton_hasMeaningfulDescription() {
composeTestRule.setContent {
TaskItem(task = sampleTask, onToggle = {}, onDelete = {})
}
composeTestRule
.onNodeWithContentDescription("Delete task")
.assertExists()
.assertHasClickAction()
}
Common Rationalizations
| Shortcut | Why It Fails |
|---|---|
| "We'll add accessibility later" | Retrofitting is 5-10x harder than building accessible from the start. |
| "Our users don't use TalkBack" | 15% of the world's population has a disability. You don't know your users. |
| "ContentDescription is visual boilerplate" | It's the only way screen reader users interact with your app. |
| "Material components handle accessibility" | Material provides a foundation. You still need content descriptions, focus order, and semantic grouping. |
| "Touch targets look too big" | The visual element can be 24dp. The touch target must be 48dp. They're independent. |
Red Flags
- Images or icons without content descriptions
- Touch targets smaller than 48dp
- Color as the only differentiator
- Missing heading semantics on section titles
- No
mergeDescendantson compound components - Hardcoded colors that may not meet contrast ratios
- No TalkBack or Accessibility Scanner testing before release
contentDescription = ""(should benullfor decorative)
Verification
- All meaningful images/icons have descriptive
contentDescription - Decorative elements use
contentDescription = null - All touch targets >= 48dp x 48dp
- Color is never the only differentiator
- Section headings use
semantics { heading() } - Compound components use
mergeDescendants = true - Dynamic content uses
liveRegion - Focus order matches logical reading order
- TalkBack tested on all screens
- Accessibility Scanner reports zero critical issues
- Compose tests verify content descriptions exist