Mobile Accessibility Skill
Comprehensive mobile accessibility audit and implementation for VoiceOver (iOS), TalkBack (Android), and touch-based screen reader navigation.
When to Use
Use /mobile-accessibility when you need to:
- Audit mobile interface for accessibility issues
- Implement VoiceOver/TalkBack support
- Fix focus management in modals and bottom sheets
- Add screen reader announcements for dynamic content
- Ensure touch targets meet WCAG 2.1 mobile criteria
- Support switch control and alternative input methods
Instructions
Phase 1: Mobile Accessibility Audit
Goal: Identify mobile-specific accessibility issues
WCAG 2.1 Mobile Criteria:
| Criterion | Level | Requirement |
|---|
| 2.5.5 Target Size | AAA | 44x44 CSS pixels minimum |
| 2.5.1 Pointer Gestures | A | Multi-point/path-based gestures have single-point alternative |
| 2.5.2 Pointer Cancellation | A | Completion on up-event, can abort |
| 2.5.3 Label in Name | A | Accessible name contains visible text |
| 2.5.4 Motion Actuation | A | Motion-triggered functions can be disabled |
| 1.3.4 Orientation | AA | Content works in portrait and landscape |
| 1.4.10 Reflow | AA | Content reflows to 320px without horizontal scroll |
I will:
- Check all interactive element sizes (44x44px minimum)
- Verify screen reader labels are present and descriptive
- Test focus order and keyboard navigation
- Audit ARIA landmarks and roles
- Check color contrast ratios (4.5:1 for normal text)
- Verify form inputs have associated labels
- Check that dynamic content announces to screen readers
Audit bash commands:
bash
1# Find small touch targets
2grep -rn "width:\s*[0-3][0-9]px\|height:\s*[0-3][0-9]px" --include="*.css" src/
3
4# Find images without alt text
5grep -rn "<img" --include="*.tsx" --include="*.jsx" src/ | grep -v "alt="
6
7# Find buttons without accessible labels
8grep -rn "<button" --include="*.tsx" src/ | grep -v "aria-label\|>.*</"
9
10# Find inputs without labels
11grep -rn "<input" --include="*.tsx" src/ | grep -v "aria-label\|id="
Phase 2: Screen Reader Support
Goal: Ensure VoiceOver and TalkBack can navigate and understand content
ARIA Landmarks for Mobile:
html
1<!-- Header with navigation -->
2<header role="banner">
3 <nav role="navigation" aria-label="Main navigation">
4 <!-- Nav items -->
5 </nav>
6</header>
7
8<!-- Main content -->
9<main role="main">
10 <h1>Page Title</h1>
11 <!-- Content -->
12</main>
13
14<!-- Search -->
15<div role="search">
16 <label for="search">Search</label>
17 <input type="search" id="search" />
18</div>
19
20<!-- Footer -->
21<footer role="contentinfo">
22 <!-- Footer content -->
23</footer>
Button Accessibility:
html
1<!-- ❌ Bad - no label for icon-only button -->
2<button>
3 <svg><!-- icon --></svg>
4</button>
5
6<!-- ✅ Good - aria-label for screen readers -->
7<button aria-label="Close dialog">
8 <svg aria-hidden="true"><!-- icon --></svg>
9</button>
10
11<!-- ✅ Good - visually hidden text -->
12<button>
13 <svg aria-hidden="true"><!-- icon --></svg>
14 <span class="visuallyHidden">Close dialog</span>
15</button>
Visually Hidden CSS:
css
1.visuallyHidden {
2 position: absolute;
3 width: 1px;
4 height: 1px;
5 padding: 0;
6 margin: -1px;
7 overflow: hidden;
8 clip: rect(0, 0, 0, 0);
9 white-space: nowrap;
10 border-width: 0;
11}
Form Accessibility:
html
1<!-- ❌ Bad - placeholder is not a label -->
2<input type="email" placeholder="Email address" />
3
4<!-- ✅ Good - proper label association -->
5<label for="email">Email address</label>
6<input type="email" id="email" autocomplete="email" />
7
8<!-- ✅ Good - error announcement -->
9<label for="password">Password</label>
10<input
11 type="password"
12 id="password"
13 aria-invalid="true"
14 aria-describedby="password-error"
15/>
16<div id="password-error" role="alert">
17 Password must be at least 8 characters
18</div>
Phase 3: Focus Management
Goal: Proper focus handling for modals, sheets, and navigation
Modal/Sheet Focus Management:
javascript
1class AccessibleModal {
2 constructor(modalElement) {
3 this.modal = modalElement;
4 this.focusableElements = null;
5 this.previousFocus = null;
6 }
7
8 open() {
9 // Store current focus
10 this.previousFocus = document.activeElement;
11
12 // Get all focusable elements in modal
13 this.focusableElements = this.modal.querySelectorAll(
14 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
15 );
16
17 // Focus first element
18 if (this.focusableElements.length > 0) {
19 this.focusableElements[0].focus();
20 }
21
22 // Trap focus within modal
23 this.modal.addEventListener('keydown', this.trapFocus.bind(this));
24
25 // Announce modal to screen readers
26 this.modal.setAttribute('aria-modal', 'true');
27 this.modal.setAttribute('role', 'dialog');
28 }
29
30 trapFocus(e) {
31 if (e.key !== 'Tab') return;
32
33 const firstElement = this.focusableElements[0];
34 const lastElement = this.focusableElements[this.focusableElements.length - 1];
35
36 if (e.shiftKey && document.activeElement === firstElement) {
37 e.preventDefault();
38 lastElement.focus();
39 } else if (!e.shiftKey && document.activeElement === lastElement) {
40 e.preventDefault();
41 firstElement.focus();
42 }
43 }
44
45 close() {
46 this.modal.removeEventListener('keydown', this.trapFocus);
47
48 // Return focus to trigger element
49 if (this.previousFocus) {
50 this.previousFocus.focus();
51 }
52 }
53}
Bottom Sheet Focus:
css
1.bottomSheet {
2 position: fixed;
3 bottom: 0;
4 left: 0;
5 right: 0;
6 background: var(--color-bg);
7 border-radius: 16px 16px 0 0;
8 padding: 24px;
9 transform: translateY(100%);
10 transition: transform 0.3s ease;
11}
12
13.bottomSheet.open {
14 transform: translateY(0);
15}
16
17/* Focus indicator for keyboard users */
18.bottomSheet *:focus {
19 outline: 2px solid var(--color-primary);
20 outline-offset: 2px;
21}
22
23/* Hide focus outline for mouse users, show for keyboard */
24.bottomSheet *:focus:not(:focus-visible) {
25 outline: none;
26}
27
28.bottomSheet *:focus-visible {
29 outline: 2px solid var(--color-primary);
30 outline-offset: 2px;
31}
Phase 4: Live Regions & Announcements
Goal: Announce dynamic content changes to screen readers
Toast/Snackbar Announcements:
html
1<!-- Polite announcement - doesn't interrupt -->
2<div role="status" aria-live="polite" aria-atomic="true" class="visuallyHidden">
3 <!-- Announcement text injected here -->
4</div>
5
6<!-- Assertive announcement - interrupts immediately -->
7<div role="alert" aria-live="assertive" aria-atomic="true" class="visuallyHidden">
8 <!-- Error messages injected here -->
9</div>
javascript
1function announce(message, priority = 'polite') {
2 const liveRegion = document.querySelector(`[aria-live="${priority}"]`);
3
4 // Clear and re-add to trigger announcement
5 liveRegion.textContent = '';
6 setTimeout(() => {
7 liveRegion.textContent = message;
8 }, 100);
9
10 // Auto-clear after announcement
11 setTimeout(() => {
12 liveRegion.textContent = '';
13 }, 5000);
14}
15
16// Usage
17saveButton.addEventListener('click', async () => {
18 await saveData();
19 announce('Changes saved successfully', 'polite');
20});
21
22deleteButton.addEventListener('click', async () => {
23 try {
24 await deleteItem();
25 announce('Item deleted', 'polite');
26 } catch (error) {
27 announce('Error: Could not delete item', 'assertive');
28 }
29});
Loading States:
html
1<!-- Loading button with status -->
2<button
3 aria-label="Save changes"
4 aria-busy="true"
5 disabled
6>
7 <span aria-hidden="true">
8 <!-- Spinner icon -->
9 </span>
10 <span>Saving...</span>
11</button>
12
13<!-- Completed state -->
14<button aria-label="Save changes">
15 Save
16</button>
Dynamic Content:
html
1<!-- List that updates -->
2<ul role="list" aria-live="polite" aria-relevant="additions removals">
3 <li>Item 1</li>
4 <li>Item 2</li>
5 <!-- New items announced automatically -->
6</ul>
7
8<!-- Pagination -->
9<nav aria-label="Pagination">
10 <button aria-label="Previous page" disabled>Previous</button>
11 <span aria-current="page" aria-label="Page 1 of 5">1</span>
12 <button aria-label="Next page, Page 2">Next</button>
13</nav>
Phase 5: VoiceOver/TalkBack Testing
Goal: Manual testing with actual screen readers
VoiceOver Gestures (iOS):
- Swipe right: Next item
- Swipe left: Previous item
- Double tap: Activate
- Three-finger swipe: Scroll
- Two-finger double tap: Magic Tap (primary action)
- Rotor: Two-finger rotation to change navigation mode
TalkBack Gestures (Android):
- Swipe right: Next item
- Swipe left: Previous item
- Double tap: Activate
- Swipe down then up: First item
- Swipe up then down: Last item
- Local context menu: Swipe up then right
Testing Checklist:
markdown
1### VoiceOver Testing (iOS)
2
3- [ ] Can navigate through all interactive elements
4- [ ] Button labels are descriptive ("Close" not "X")
5- [ ] Form inputs announce their purpose and state
6- [ ] Images have meaningful alt text (or aria-hidden if decorative)
7- [ ] Modals trap focus and announce as dialogs
8- [ ] Page title announces on navigation
9- [ ] Headings create logical document outline
10- [ ] Custom controls have appropriate ARIA roles
11- [ ] Loading states announce to user
12- [ ] Error messages are announced immediately
13- [ ] Dynamic content updates are announced
14- [ ] Can dismiss modals with swipe gestures
15- [ ] Rotor navigation works (headings, links, form controls)
16
17### TalkBack Testing (Android)
18
19- [ ] All interactive elements are reachable
20- [ ] Touch exploration works (drag finger to explore)
21- [ ] Local context menu provides actions
22- [ ] Swipe gestures navigate correctly
23- [ ] Custom gestures have TalkBack equivalents
24- [ ] Reading order matches visual order
25- [ ] Lists announce item count
26- [ ] Expandable sections announce state (expanded/collapsed)
27
28### Switch Control Testing
29
30- [ ] Can navigate with single switch
31- [ ] All actions reachable via scanning
32- [ ] Scanning speed is reasonable
33- [ ] No focus traps for switch users
Phase 6: Reduced Motion & Preferences
Goal: Respect user preferences for motion and accessibility
css
1/* Disable animations for users who prefer reduced motion */
2@media (prefers-reduced-motion: reduce) {
3 *,
4 *::before,
5 *::after {
6 animation-duration: 0.01ms !important;
7 animation-iteration-count: 1 !important;
8 transition-duration: 0.01ms !important;
9 }
10
11 .pullToRefresh .refreshIndicator,
12 .swipeableItem .swipeContent {
13 transition: none !important;
14 }
15}
16
17/* High contrast mode */
18@media (prefers-contrast: high) {
19 .button {
20 border: 2px solid currentColor;
21 }
22
23 .card {
24 border: 1px solid var(--color-text);
25 }
26}
27
28/* Dark mode preference */
29@media (prefers-color-scheme: dark) {
30 :root {
31 --color-bg: #121212;
32 --color-text: #ffffff;
33 }
34}
Mobile Accessibility Checklist
Touch Targets
Screen Reader Support
Focus Management
Live Regions
Motion & Preferences
Mobile-Specific
markdown
1## Mobile Accessibility Audit Results
2
3### Critical Issues (WCAG Level A)
4- [ ] **Touch Target Too Small** - Button at `[selector]` is 32x32px (requires 44x44px)
5- [ ] **Missing Label** - Input at `[selector]` has no associated label
6- [ ] **Focus Trap** - Modal does not return focus on close
7
8### Important Issues (WCAG Level AA)
9- [ ] **Insufficient Contrast** - Text on background is 3.2:1 (requires 4.5:1)
10- [ ] **Missing Landmark** - Page missing main landmark
11
12### Enhancements (WCAG Level AAA)
13- [ ] **Touch Target Enhancement** - Increase to 48x48px for better usability
14- [ ] **Enhanced Announcements** - Add more descriptive loading states
15
16### Files Modified
17- `[filepath]` - Touch target fixes
18- `[filepath]` - ARIA labels and landmarks
19- `[filepath]` - Focus management
20- `[filepath]` - Live region announcements
21
22### Testing Notes
23**VoiceOver (iOS):**
24- Navigation works correctly through all elements
25- Buttons announce purpose clearly
26- Modal focus trapping works
27
28**TalkBack (Android):**
29- All elements reachable via swipe
30- Local context menu provides expected actions
31- Reading order matches visual layout
32
33### Checklist Results
34✅ Touch targets ≥ 44px
35✅ Screen reader labels present
36✅ Focus management implemented
37✅ Live regions for dynamic content
38✅ Reduced motion respected
39⚠️ High contrast mode needs testing
Integration with Other Skills
- /mobile-patterns - Ensure navigation patterns are accessible
- /touch-interactions - Verify gestures have keyboard alternatives
- /component-states - Add accessible focus and disabled states
- /accessibility-audit - General accessibility (this skill is mobile-focused)
Notes
- Test with actual screen readers on real devices - simulators don't fully replicate experience
- VoiceOver and TalkBack have different behaviors - test both
- Touch target size is the #1 mobile accessibility issue
- Don't rely on color alone to convey information
- Keep forms short and use appropriate input types for mobile keyboards
- Respect user preferences (reduced motion, dark mode, high contrast)
- Live regions should be in DOM from page load, not dynamically added