<!-- skill: Mobile A11y | version: 1.0.0 | description: TalkBack, VoiceOver, contrast and dynamic type for mobile apps | category: Mobile -->
## Instructions
- Label every interactive element: Flutter `Semantics(label: 'Play episode', button: true)`, Compose `contentDescription="Play"`, SwiftUI `.accessibilityLabel("Play")` + `.accessibilityAddTraits(.isButton)`; icon-only buttons never rely on tooltip.
- Group + hide decor: `Semantics(container: true)` / `Modifier.semantics(mergeDescendantsIntoOwner=true)`, `.accessibilityElement(children: .combine)`; mark dividers/art with `excludeSemantics()` / `.accessibilityHidden(true)`.
- Traversal order: Flutter `Semantics(sortKey: OrdinalSortKey(1))` + `FocusTraversalGroup`, Compose `Modifier.focusProperties { next = ... }` + `traversalIndex`, iOS `.accessibilitySortPriority(1)`; test swipe-order matches visual top→bottom.
- Touch targets 48dp: `minTouchTargetSize` 48x48 (`IconButton` padding, Compose `.sizeIn(minWidth: 48.dp, minHeight: 48.dp)`), SwiftUI `.frame(minWidth: 44, minHeight: 44)` (Apple floor 44pt, use 48 for parity); never 32dp chips without expanded hitSlop.
- Contrast: text 4.5:1 (3:1 large ≥18pt), icons 3:1; check with Stark/axe + real devices (AMOLED + sunlight); ensure focus rings visible in dark mode too.
- Dynamic Type: Flutter `MediaQuery.textScaler` + `maxLines` resilient layouts (`FittedBox` fallback), Compose `fontScale` test 1.0→2.0, SwiftUI `.dynamicTypeSize(...)` + `minimumScaleFactor(0.8)`; no clipped CTAs at XXXL.
- Headings + state: mark headers (`heading: true` / `.accessibilityAddTraits(.isHeader)`), announce state (`selected`/`expanded`/`disabled`), actions (`accessibilityAction` / `customActions`) for swipe-to-delete etc.
- Forms/errors: `label` linked to field, `error` announced via `liveRegion` (Compose `liveRegion = LiveRegionMode.Assertive` / `UIAccessibility.post(notification: .announcement)`), move focus to first error on submit.
- Motion + haptics: respect `reduceMotion` (`MediaQuery.disableAnimations` / `UIAccessibility.isReduceMotionEnabled`) — disable parallax/autoplay; pair haptic with visual + announcement, never haptic-only confirm.
- Test matrix: TalkBack (Linear nav + Explore by touch) + VoiceOver (Rotor: headings/links) on real devices; `Accessibility Scanner` (Android) + `Accessibility Inspector` (Xcode) per PR; automated `flutter test` semantics (`expect(find.bySemanticsLabel, matchesSemantics)`).
- Keyboard/D-pad: all actions reachable via Tab/arrows + Enter; visible focus indicator; no focus traps in bottom sheets/dialogs (Esc dismisses, focus returns to trigger).
- Write acceptance per screen: "VO reads title→price→CTA in order; 48dp targets; 4.5:1; XXXL no clip; errors announced" — attach scanner screenshots to PR.
## Examples
- Player screen: combined play/pause semantics, custom actions (skip ±15s), reduced-motion stills, XXXL-safe title.
- Checkout form: labeled fields, assertive error live-region, focus to error, 48dp pay button, contrast-fixed helper text.
- Feed list: heading per section, merged card semantics with single tap, decorative thumbnails hidden.
## Workflows
1. Audit screen with Scanner/Inspector → list unlabeled/small/low-contrast issues.
2. Fix labels/groups/order + 48dp targets + contrast tokens.
3. Dynamic Type sweep (1.0/1.5/2.0) + reduce-motion + dark mode.
4. Screen-reader pass (TalkBack + VoiceOver rotor) → fix order/announcements.
5. Keyboard/D-pad + focus-return check on dialogs/sheets.
6. Attach semantics tests + scanner shots to PR, gate on zero errors.
## Templates
- Flutter + Compose semantics:
```dart
// Flutter: merged card + 48dp button + header
Semantics(
  container: true, button: true, label: 'Play: Design ep. 12, 24 minutes',
  child: InkWell(
    onTap: play,
    child: Row(children: [
      ExcludeSemantics(child: CoverArt(url: url)),
      Expanded(child: Column(children: [
        Semantics(header: true, child: Text(title, maxLines: 2)),
        Text('24 min', style: TextStyle(color: onSurfaceVariant)),
      ])),
      SizedBox(width: 48, height: 48, child: IconButton(icon: Icon(Icons.play_arrow), onPressed: play)),
    ]),
  ),
);
```
```kotlin
// Compose: label + state + live error
IconButton(onClick = play, modifier = Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp)
  .semantics { contentDescription = "Play"; role = Role.Button }) { Icon(Icons.Filled.Play, null) }
Text(text = err, modifier = Modifier.semantics { liveRegion = LiveRegionMode.Assertive },
  color = MaterialTheme.colorScheme.error)
```
```swift
// SwiftUI: label + actions + dynamic type
Button("Play") { play() }
  .accessibilityLabel("Play episode 12")
  .accessibilityHint("Starts playback")
  .frame(minWidth: 48, minHeight: 48)
  .accessibilityAction(named: "Skip forward 15 seconds") { skip(15) }
Text(title).font(.headline).accessibilityAddTraits(.isHeader)
  .dynamicTypeSize(...DynamicTypeSize.accessibility3).minimumScaleFactor(0.8)
```
## Tools
- SARAH shell_run for `flutter test` semantics, `adb` font-scale change, Xcode Accessibility Inspector.
- SARAH file_write for semantics wrappers/test helpers; web_fetch for WCAG + platform a11y docs.
## Advanced
- Rotor/custom actions: iOS `.accessibilityRotor("Sections")` entries, Android `AccessibilityAction` for archive/snooze; announce async loads (`announcement: "Loaded 20 more"`).
- Braille/captions: test BrailleBack display, video `SubRip` captions + audio-description track; never autoplay with sound.
- Metrics: track `a11y_label_coverage` lint %, scanner issue count per build, VoiceOver navigation time in usability tests.
## Documentation
- https://developer.android.com/guide/topics/ui/accessibility
- https://developer.apple.com/accessibility/
- https://docs.flutter.dev/ui/accessibility
- https://www.w3.org/WAI/WCAG21/quickref/
