Mobile Accessibility Skill
This skill provides mobile accessibility reference data for React Native, Expo, iOS, and Android auditing. Used by mobile-accessibility.agent.md.
React Native Accessibility Props - Full Reference
Each prop, with its type, values or notes, WCAG SC and required.
| Prop | Type | Values / Notes | WCAG SC | Required |
|---|---|---|---|---|
accessible |
boolean | true marks the view as an accessibility node |
4.1.2 | Conditional |
accessibilityLabel |
string | Human-readable name - overrides all child text | 1.1.1, 4.1.2 | Yes (all interactive + image elements) |
accessibilityLabelledBy |
string / string[] | References ID(s) of labelling element | 1.3.1 | For form inputs |
accessibilityRole |
string (see roles table) | Communicates element type to AT | 4.1.2 | Yes (all interactive elements) |
accessibilityHint |
string | Additional context spoken after label + role | 1.3.3 | When action isn't obvious |
accessibilityState |
object | {checked, disabled, expanded, selected, busy} |
4.1.2 | State-bearing elements |
accessibilityValue |
object | {min, max, now, text} |
1.3.1 | Sliders, steppers, progress bars |
accessibilityActions |
array | [{name, label}] - defines custom actions |
4.1.3 | Context menus, long-press alternatives |
onAccessibilityAction |
function | Handles custom action triggers | 4.1.3 | Paired with accessibilityActions |
accessibilityLiveRegion |
string | 'none' / 'polite' / 'assertive' |
4.1.3 | Dynamic content updates |
accessibilityViewIsModal |
boolean | true traps VoiceOver focus inside modal |
1.3.4 | Modals, drawers, sheets |
accessibilityElementsHidden |
boolean | iOS - hides element and children from VoiceOver | 1.1.1 | Decorative elements |
importantForAccessibility |
string | Android - 'auto' / 'yes' / 'no' / 'no-hide-descendants' |
1.1.1 | Decorative / grouped elements |
accessibilityIgnoresInvertColors |
boolean | iOS - preserves colors in Inverted Colors mode | - | Images, video |
aria-label |
string | RN 0.73+ alias for accessibilityLabel |
1.1.1, 4.1.2 | Preferred in new code |
aria-labelledby |
string | RN 0.73+ alias for accessibilityLabelledBy |
1.3.1 | Form inputs |
aria-describedby |
string | RN 0.73+ alias for accessibilityHint |
1.3.3 | Additional description |
aria-role |
string | RN 0.73+ alias for accessibilityRole |
4.1.2 | All interactive elements |
aria-checked |
boolean / 'mixed' | RN 0.73+ alias for accessibilityState.checked |
4.1.2 | Checkboxes |
aria-disabled |
boolean | RN 0.73+ alias for accessibilityState.disabled |
4.1.2 | Disabled elements |
aria-expanded |
boolean | RN 0.73+ alias for accessibilityState.expanded |
4.1.2 | Accordions, dropdowns |
aria-selected |
boolean | RN 0.73+ alias for accessibilityState.selected |
4.1.2 | Tabs, list items |
aria-busy |
boolean | RN 0.73+ alias for accessibilityState.busy |
4.1.3 | Loading elements |
aria-hidden |
boolean | RN 0.73+ - maps to importantForAccessibility / accessibilityElementsHidden |
1.1.1 | Decorative content |
aria-live |
string | RN 0.73+ alias for accessibilityLiveRegion |
4.1.3 | Dynamic content |
aria-modal |
boolean | RN 0.73+ alias for accessibilityViewIsModal |
1.3.4 | Modals |
Accessibility Role Values
Each role, with maps to (ios), maps to (android) and use for.
| Role | Maps to (iOS) | Maps to (Android) | Use For |
|---|---|---|---|
'button' |
UIAccessibilityTraitButton | AccessibilityNodeInfo.ROLE_BUTTON | Buttons, submission triggers |
'link' |
UIAccessibilityTraitLink | AccessibilityNodeInfo.ROLE_LINK | Navigation links, external URLs |
'search' |
- | ROLE_SEARCH | Search bars |
'image' |
UIAccessibilityTraitImage | ROLE_IMAGE | Images (when accessible=true) |
'imagebutton' |
UIAccessibilityTraitImage+Button | ROLE_BUTTON | Icon buttons |
'header' |
UIAccessibilityTraitHeader | ROLE_HEADING | Headings |
'text' |
UIAccessibilityTraitStaticText | ROLE_LABEL | Static text |
'adjustable' |
UIAccessibilityTraitAdjustable | ROLE_SCROLL_VIEW | Sliders |
'checkbox' |
- | ROLE_CHECKBOX | Checkboxes |
'combobox' |
- | ROLE_DROP_DOWN_LIST | Dropdowns |
'menu' |
- | ROLE_MENU | Menus |
'menuitem' |
- | ROLE_MENU_ITEM | Menu items |
'menubar' |
- | ROLE_MENU_BAR | Menu bars |
'progressbar' |
UIAccessibilityTraitUpdatesFrequently | ROLE_PROGRESS_BAR | Progress indicators |
'radio' |
- | ROLE_RADIO_BUTTON | Radio buttons |
'radiogroup' |
- | - | Radio button groups |
'scrollbar' |
- | ROLE_SCROLL_BAR | Scrollbars |
'spinbutton' |
- | ROLE_SCROLL_VIEW | Steppers, number inputs |
'switch' |
- | ROLE_SWITCH | Toggle switches |
'tab' |
- | ROLE_TAB | Tab elements |
'tablist' |
- | ROLE_TAB_LIST | Tab containers |
'timer' |
UIAccessibilityTraitUpdatesFrequently | - | Countdown timers |
'toolbar' |
- | ROLE_TOOL_BAR | Toolbars |
'grid' |
- | ROLE_GRID | Data grids |
'list' |
- | ROLE_LIST | Lists |
'listitem' |
- | ROLE_LIST_ITEM | List items |
'summary' |
UIAccessibilityTraitSummaryElement | - | Summary/status views |
'alert' |
UIAccessibilityTraitCausesPageTurn | ROLE_ALERT | Alert dialogs |
'none' |
UIAccessibilityTraitNone | ROLE_NONE | Suppress role |
Touch Target Size Requirements
Each platform, with its minimum size, recommended and standard.
| Platform | Minimum Size | Recommended | Standard |
|---|---|---|---|
| iOS | 44 x 44 pt | 44 x 44 pt | HIG |
| Android | 48 x 48 dp | 48 x 48 dp | Material Design |
| Web mobile | 44 x 44 CSS px (AAA) | 44 x 44 CSS px | WCAG 2.5.5 |
| Web mobile (AA, 2.2) | 24 x 24 CSS px with spacing | 44 x 44 CSS px | WCAG 2.5.8 |
Detection Pattern (React Native)
// Violation: TouchableOpacity below minimum
const styles = StyleSheet.create({
closeBtn: { width: 24, height: 24 }, // FAIL - below 44pt
iconBtn: { width: 32, height: 32 }, // FAIL - below 44pt
navBtn: { padding: 4 }, // CONDITIONAL - depends on content size
compliant: { width: 44, height: 44 }, // PASS
compliantWithPadding: { padding: 12 }, // PASS if content >= 20pt
});
iOS UIAccessibility - Quick Reference
SwiftUI Modifiers
Each modifier, with its purpose.
| Modifier | Purpose |
|---|---|
.accessibilityLabel("...") |
Overrides spoken name |
.accessibilityHint("...") |
Spoken usage hint (after pause) |
.accessibilityValue("...") |
Spoken current value |
.accessibilityHidden(true) |
Removes from VoiceOver tree |
.accessibilityElement(children: .combine) |
Merges child elements into one node |
.accessibilityElement(children: .contain) |
Groups children as sub-elements |
.accessibilityElement(children: .ignore) |
Container becomes accessible, children hidden |
.accessibilityAddTraits(.isButton) |
Adds role trait |
.accessibilityRemoveTraits(.isImage) |
Removes wrong role trait |
.accessibilityInputLabels(["..."]) |
Voice Control activation labels |
.accessibilitySortPriority(n) |
Overrides VoiceOver reading order (higher = earlier) |
.accessibilityAction(named: "...", {}) |
Custom action in the Actions rotor |
.accessibilityActivationPoint(CGPoint) |
Override activation tap point |
.accessibilityCustomContent("label", "value") |
Extra info in Accessibility Inspector |
SwiftUI Trait Values
isButton, isHeader, isLink, isImage, isStaticText, isSelected, isKeyboardKey, isSearchField, playsSound, isModal, updatesFrequently, startsMediaSession, allowsDirectInteraction, causesPageTurn, isTabBar, isSummaryElement
UIKit Properties
Each property, with its type and notes.
| Property | Type | Notes |
|---|---|---|
isAccessibilityElement |
Bool | Set true on custom views |
accessibilityLabel |
String? | Overrides spoken name |
accessibilityHint |
String? | Spoken after pause |
accessibilityValue |
String? | Current value (sliders, progress) |
accessibilityTraits |
UIAccessibilityTraits | Bitfield of traits (.button, .header, etc.) |
accessibilityFrame |
CGRect | Determines VoiceOver focus rect |
accessibilityActivate() |
func | Override activation behavior |
accessibilityElements |
[Any]? | Set container's VoiceOver child order |
shouldGroupAccessibilityChildren |
Bool | Groups all children into single node |
accessibilityViewIsModal |
Bool | true = VoiceOver trapped inside |
accessibilityElementsHidden |
Bool | Hides all children from VoiceOver |
Android Jetpack Compose Semantics - Quick Reference
Each modifier / property, with its purpose.
| Modifier / Property | Purpose |
|---|---|
semantics { contentDescription = "..." } |
Accessible name |
semantics { role = Role.Button } |
Element role |
semantics { stateDescription = "..." } |
Current state text |
semantics { heading() } |
Marks as heading |
semantics { selected = true/false } |
Selected state |
semantics { toggleableState = ToggleableState.On } |
Toggle state |
semantics { onClick(label = "...", action = {...}) } |
Click action with label |
semantics { disabled() } |
Disabled state |
semantics { focused = true } |
Force focus |
semantics { liveRegion = LiveRegion.Polite } |
Live region announcements |
semantics { invisibleToUser() } |
Hide from TalkBack |
semantics { mergeDescendants = true } |
Merge child semantics into one node |
clearAndSetSemantics { ... } |
Replace all descendant semantics |
Modifier.semantics(mergeDescendants = true) { } |
Short merge pattern |
Role Values
Role.Button, Role.Checkbox, Role.DropdownList, Role.Image, Role.RadioButton, Role.Switch, Role.Tab
Common Violation Patterns and Fixes
RN-001: Missing accessibilityLabel on icon button
// VIOLATION
<TouchableOpacity onPress={close}>
<Icon name="x" size={20} />
</TouchableOpacity>
// FIX
<TouchableOpacity
onPress={close}
accessibilityRole="button"
accessibilityLabel="Close"
>
<Icon name="x" size={20} aria-hidden />
</TouchableOpacity>
RN-002: Image missing label
// VIOLATION
<Image source={productImage} style={styles.product} />
// FIX - informational image
<Image
source={productImage}
style={styles.product}
accessibilityLabel="Blue suede shoes, size 10"
/>
// FIX - decorative image
<Image
source={decorativeBackground}
style={styles.bg}
accessible={false}
importantForAccessibility="no"
/>
RN-003: TextInput missing label
// VIOLATION - placeholder is not a label
<TextInput placeholder="Email" value={email} onChangeText={setEmail} />
// FIX
<View>
<Text nativeID="emailLabel">Email address</Text>
<TextInput
value={email}
onChangeText={setEmail}
accessibilityLabelledBy="emailLabel"
accessibilityHint="Enter your email address"
keyboardType="email-address"
autoComplete="email"
/>
</View>
RN-004: Checkbox missing state
// VIOLATION
<TouchableOpacity onPress={toggle}>
<Image source={checked ? checkedIcon : uncheckedIcon} />
<Text>Accept terms</Text>
</TouchableOpacity>
// FIX
<TouchableOpacity
onPress={toggle}
accessibilityRole="checkbox"
accessibilityState={{ checked }}
accessibilityLabel="Accept terms and conditions"
>
<Image source={checked ? checkedIcon : uncheckedIcon} accessible={false} />
<Text>Accept terms</Text>
</TouchableOpacity>
RN-005: Modal not trapping focus
// VIOLATION - custom modal without VoiceOver trap
<View style={styles.modal}>
<Text>Are you sure?</Text>
<Button title="Confirm" onPress={confirm} />
</View>
// FIX - use Modal component (traps focus automatically) or set accessibilityViewIsModal
<Modal
visible={visible}
transparent
accessibilityViewIsModal={true} // traps VoiceOver
onRequestClose={close} // Android back button
>
<View style={styles.overlay}>
<Text>Are you sure?</Text>
<Button title="Confirm" onPress={confirm} />
<Button title="Cancel" onPress={close} />
</View>
</Modal>
AND-001: Compose image missing content description
// VIOLATION
Image(
painter = painterResource(id = R.drawable.product),
contentDescription = null // null = decorative, but wrong for informational image
)
// FIX - informational
Image(
painter = painterResource(id = R.drawable.product),
contentDescription = stringResource(R.string.product_image_description)
)
// FIX - truly decorative
Image(
painter = painterResource(id = R.drawable.divider),
contentDescription = null,
modifier = Modifier.semantics { invisibleToUser() }
)
Testing Tool Commands
iOS Accessibility Inspector (Xcode)
Xcode -> Xcode menu -> Open Developer Tool -> Accessibility Inspector
- Run audit: Click "Audit" tab -> "Run Audit" button
- Inspect: "Inspection" tab -> hover over elements in Simulator
- Keyboard: Use +F7 to toggle VoiceOver in Simulator
Android Accessibility Scanner
# Install (Play Store or adb)
adb install com.google.android.apps.accessibility.auditor
# Enable TalkBack via ADB (for CI)
adb shell settings put secure enabled_accessibility_services \
com.google.android.marvin.talkback/.TalkBackService
# Check accessibility node tree
adb shell uiautomator dump /sdcard/ui_dump.xml
adb pull /sdcard/ui_dump.xml
React Native Testing Library
npm install --save-dev @testing-library/react-native
import { render, screen, fireEvent } from '@testing-library/react-native';
test('button has accessible name and role', () => {
render(<SubmitButton onPress={jest.fn()} />);
const btn = screen.getByRole('button', { name: /submit/i });
expect(btn).toBeTruthy();
});
test('checkbox updates state', () => {
render(<TermsCheckbox />);
const checkbox = screen.getByRole('checkbox', { name: /accept terms/i });
expect(checkbox).toHaveAccessibilityState({ checked: false });
fireEvent.press(checkbox);
expect(checkbox).toHaveAccessibilityState({ checked: true });
});
Maestro (E2E)
# .maestro/accessibility-checks.yaml
appId: com.example.myapp
---
- launchApp
- assertVisible:
label: "Submit form"
- tapOn:
label: "Close"
- assertNotVisible:
label: "Are you sure?"