Compose accessibility
Skill almasumdev/awesome-kotlin-android-agent-skills/.github/skills/ui/compose-accessibility
Curated agent skills, conventions, and workflows for building Kotlin Android apps with AI coding agents.
npx -y skills add almasumdev/awesome-kotlin-android-agent-skills --skill compose-accessibilityAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 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
Expert guidance on building accessible Compose UIs with semantics modifiers, TalkBack support, correct touch target sizing, contrast, and large-text scaling. Use this when authoring or reviewing any user-facing composable.
SKILL.md
4.3 KB, as published. Nobody here has run it
Accessibility in Jetpack Compose
Instructions
Accessibility is not optional. Compose exposes a tree of semantics nodes to TalkBack and automated testing. Correct semantics unlock both.
1. Semantics Modifier Basics
IconButton(
onClick = onDelete,
modifier = Modifier.semantics { contentDescription = stringResource(R.string.delete_item) },
) {
Icon(Icons.Default.Delete, contentDescription = null)
}
Rules:
- Use the built-in
contentDescriptionparameter onIcon,Image, andAsyncImage— it already writes the semantic property correctly. - Set
contentDescription = nullfor purely decorative imagery. - Never embed a period in short labels; TalkBack handles pauses automatically.
2. Merging vs Clearing
Row(Modifier
.clickable(onClick = onOpen)
.semantics(mergeDescendants = true) {
contentDescription = "Unread message from ${msg.sender}: ${msg.preview}"
}
) {
Avatar(msg.sender)
Column { Text(msg.sender); Text(msg.preview) }
}
mergeDescendants = true collapses children into a single focus item, which is exactly what TalkBack users expect for a list row.
3. Roles and State
var checked by remember { mutableStateOf(false) }
Box(
Modifier
.toggleable(
value = checked,
role = Role.Checkbox,
onValueChange = { checked = it },
)
.size(48.dp),
) { /* custom visual */ }
Prefer toggleable, selectable, clickable(role = Role.Button), progressSemantics() over ad-hoc semantics { } blocks.
4. Headings and Live Regions
Text("Today", modifier = Modifier.semantics { heading() }, style = MaterialTheme.typography.titleLarge)
Text(
text = status,
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
)
5. Touch Targets
Material 3 already ships 48 dp defaults for Button, IconButton, Checkbox. For custom widgets, enforce it:
Box(Modifier.minimumInteractiveComponentSize().clickable(onClick = onTap)) { /* ... */ }
minimumInteractiveComponentSize() expands the touch target to at least 48 dp without changing the visual size.
6. Large Text and Dynamic Scaling
Never hard-code sp→dp. Let text scale. Test your screens at 1.3× and 2.0× font scale:
adb shell settings put system font_scale 2.0
Use TextUnit math: MaterialTheme.typography.bodyMedium.fontSize * 1.2. Prefer lineHeight = with(LocalDensity.current) { ... } relative to font size.
7. Color Contrast
- Body text vs background: 4.5 : 1 minimum.
- Large text (≥ 18 sp regular or ≥ 14 sp bold): 3 : 1.
- Never encode meaning in color alone — pair with an icon or label.
Use Material 3 colorScheme.onSurface / colorScheme.onPrimary pairs; they are guaranteed to meet contrast against their parent color roles.
8. Custom Actions for Lists
Row(Modifier.semantics {
customActions = listOf(
CustomAccessibilityAction(label = "Archive") { onArchive(msg); true },
CustomAccessibilityAction(label = "Delete") { onDelete(msg); true },
)
}) { /* visual swipe actions */ }
This exposes swipe-hidden actions to TalkBack users.
9. Testing Semantics
composeRule.onNodeWithContentDescription("Delete item").performClick()
composeRule.onNode(hasText("Today") and hasAnyAncestor(isHeading())).assertIsDisplayed()
Run the Accessibility Scanner app on CI-produced screenshots and fix every "Missing label" / "Low contrast" finding.
Checklist
- Every interactive element has a label (either
contentDescriptionor visibleText). - Purely decorative icons/images pass
contentDescription = null. - Custom tap targets use
minimumInteractiveComponentSize()or are at least 48 dp. - Headings use
Modifier.semantics { heading() }. - Screens render correctly at
font_scale = 2.0with no truncation. - Color pairs meet WCAG AA contrast; meaning is never carried by color alone.
- Compose UI tests use semantics matchers rather than tag-only lookups.