agentsclimarketplace

Kmp accessibility

Skill almasumdev/awesome-kotlin-multiplatform-agent-skills/.github/skills/ui/kmp-accessibility

Curated agent skills, conventions, and workflows for building Kotlin Multiplatform (KMP) apps with AI coding agents.

Install
npx -y skills add almasumdev/awesome-kotlin-multiplatform-agent-skills --skill kmp-accessibility

Assembled 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

Accessibility in Compose Multiplatform — semantics, focus order, contrast, dynamic type, and the places where Android TalkBack and iOS VoiceOver diverge. Use when building or auditing shared UI.

SKILL.md

4.5 KB, as published. Nobody here has run it

KMP Accessibility

Instructions

Compose Multiplatform maps Modifier.semantics to Android's AccessibilityNodeInfo and iOS's UIAccessibility. Most concepts transfer, but a few traits need platform-aware handling.

1. Core semantics

IconButton(
    onClick = onFavorite,
    modifier = Modifier.semantics {
        contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites"
        role = Role.Button
        stateDescription = if (isFavorite) "Favorited" else "Not favorited"
    },
) {
    Icon(if (isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, contentDescription = null)
}
  • Set contentDescription = null on decorative icons inside a labelled parent.
  • Use stateDescription for toggle state — both TalkBack and VoiceOver read it.
  • Prefer Role.Button, Role.Switch, Role.Checkbox so the platform announces the control type.

2. Headings & grouping

Text(
    "Account",
    style = MaterialTheme.typography.headlineSmall,
    modifier = Modifier.semantics { heading() },
)

Row(
    modifier = Modifier.semantics(mergeDescendants = true) {
        contentDescription = "Battery: 84 percent, charging"
    },
) {
    Icon(Icons.Filled.BatteryChargingFull, contentDescription = null)
    Text("84%")
}

mergeDescendants = true is the KMP equivalent of iOS isAccessibilityElement = true — essential for composite controls.

3. Touch target size

Platform minimums differ: Android 48dp, iOS 44pt. Use the larger:

Modifier.minimumInteractiveComponentSize() // Material 3 helper, defaults to 48.dp

Wrap tap targets that look smaller than the hit box:

Box(
    modifier = Modifier
        .size(48.dp)
        .clickable(onClickLabel = "Dismiss") { onDismiss() }
        .padding(12.dp),
) { Icon(Icons.Filled.Close, contentDescription = null) }

4. Dynamic type & contrast

  • Support user font scale: avoid Modifier.size(...) on text containers; use wrapContentHeight.
  • Contrast: ensure onSurface/onPrimary token pairs meet 4.5:1 for text. Material 3 defaults comply, but branded themes often don't — verify with tooling.
  • iOS has Bold Text and Increase Contrast system settings; these do not automatically propagate to Compose MP. Observe via UIAccessibilityIsBoldTextEnabled() in iosMain and expose as a StateFlow<AccessibilityPrefs>.

5. Focus & traversal

val focusManager = LocalFocusManager.current
OutlinedTextField(
    value = email, onValueChange = onEmailChange,
    keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
    keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }),
)

For custom traversal order, use Modifier.semantics { traversalIndex = 1f } — supported on both Android and iOS in CMP 1.7+.

6. Live regions & announcements

// Shared
val announcer = LocalAccessibilityAnnouncer.current // custom CompositionLocal
LaunchedEffect(message) { message?.let { announcer.announce(it) } }

Back it with platform implementations:

// androidMain
class AndroidAnnouncer(private val view: View) : AccessibilityAnnouncer {
    override fun announce(text: String) = view.announceForAccessibility(text)
}
// iosMain
class IosAnnouncer : AccessibilityAnnouncer {
    override fun announce(text: String) =
        UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, text)
}

7. Testing

  • Android: accessibility-test-framework + Espresso.
  • iOS: XCUIElement.isAccessibilityElement assertions in XCTest, driven against the Compose view controller.
  • Shared: semantic assertions via composeTestRule.onNodeWithContentDescription("…") work on every target.

Checklist

  • Every interactive control has a contentDescription or visible label merged into its semantics.
  • Decorative Icons pass contentDescription = null.
  • Composite rows use mergeDescendants = true with a coherent description.
  • Tap targets are at least 48dp / 44pt.
  • Dynamic type respected — no fixed-height text containers.
  • Live region announcements go through a shared abstraction with Android/iOS actuals.
  • Tested with TalkBack on Android and VoiceOver on iOS on a real device.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.