Skip to content
Back to skills

Android Accessibility

ASecurity

Use when building or reviewing Android UI for accessibility. Covers TalkBack, content descriptions, touch targets, Compose semantics, focus order, and Accessibility Scanner testing.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 4, 2026
ai-agentsgokotlinbashnodetesting

Works with

  • cli

Security analysis

A100/100

Scanned October 4, 2026

npx -y skills add GuillemRoca/agent-skills-android --skill android-accessibility --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Android Accessibility?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Android Accessibility
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/guillemroca-android-accessibility/badge)](https://www.skillsdirectory.com/skills/guillemroca-android-accessibility)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: android-accessibility
description: >-
  Use when building or reviewing Android UI for accessibility. Covers
  TalkBack, content descriptions, touch targets, Compose semantics,
  focus order, and Accessibility Scanner testing.
---

# 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

1. **Every meaningful visual element needs a description:**

```kotlin
// 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
    )
}
```

2. **Description rules:**
   - Describe the **action or meaning**, not the visual appearance
   - "Delete task" not "red trash can icon"
   - "Search" not "magnifying glass"
   - Use `null` for purely decorative elements
   - Include dynamic content: "Profile photo of John" not just "profile photo"

### Step 2: Touch Targets

3. **Enforce minimum 48dp touch targets:**

```kotlin
// 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

4. **Use semantics for rich accessibility information:**

```kotlin
// 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 },
)
```

5. **Semantics rules:**
   - `mergeDescendants = true` for compound components (card with title + subtitle)
   - `heading()` for section titles (enables TalkBack heading navigation)
   - `liveRegion` for content that updates dynamically (counters, status)
   - `stateDescription` for custom state (not just "checked/unchecked")
   - `Role` for custom interactive elements

### Step 4: Focus Order

6. **Ensure logical focus order:**

```kotlin
// 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

7. **Don't rely on color alone:**

```kotlin
// 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,
        )
    }
}
```

8. **Use Material theme colors** — they're designed for contrast compliance:
   - `onSurface` on `surface` — guaranteed contrast
   - `onPrimary` on `primary` — guaranteed contrast
   - `error` and `onError` — guaranteed contrast

### Step 6: Testing

9. **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

10. **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

11. **`android layout` for scriptable accessibility assertions:**

    `android layout --pretty` returns JSON with `content-desc`, `role`, and `bounds` per node — turn it into automated scans you can run from a script or CI step.

    ```bash
    # 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.sh
    ```

    Use `android layout --diff` after an interaction to inspect only what changed — keeps assertions tight when verifying live regions, error toasts, or focus shifts. When `layout` returns nothing useful (WebView, animation), fall back to `android screen capture --annotate -o screen.png` and visually verify focus order. See `../../references/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 `@Preview` as JSON — assert on `contentDescription`, `role`, and heading semantics straight from the preview, before anything is installed (requires a running Android Studio with Gemini; see `../../references/android-cli-reference.md`).

12. **Compose UI tests for accessibility:**

```kotlin
@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 `mergeDescendants` on compound components
- Hardcoded colors that may not meet contrast ratios
- No TalkBack or Accessibility Scanner testing before release
- `contentDescription = ""` (should be `null` for 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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…