React Native / Expo Accessibility Skill
This skill implements and audits accessibility (a11y) in React Native and Expo apps, following
WCAG 2.1 AA standards as adapted for mobile (iOS VoiceOver + Android TalkBack).
When This Skill Is Needed
Use this skill for:
- Implementing
accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityState, accessibilityValue
- Auditing a component, screen, or entire app for a11y gaps
- Fixing VoiceOver / TalkBack focus order and grouping
- Checking color contrast and tap target sizes
- Writing accessible custom components (modals, carousels, date pickers, etc.)
- Integrating automated a11y testing (eslint-plugin-jsx-a11y adapted for RN, react-native-a11y)
- Adding
AccessibilityInfo API usage (reduce motion, screen reader detection)
Core Principles
- Every interactive element must have an accessible name — via
accessibilityLabel or meaningful text child.
- Every interactive element must have a role —
accessibilityRole tells screen readers what it is.
- State must be announced — use
accessibilityState for disabled, selected, checked, expanded, busy.
- Logical reading order — use
importantForAccessibility (Android) and accessibilityViewIsModal to manage focus scope.
- Tap targets ≥ 44×44 pts — per Apple HIG and Android guidelines.
- 4.5:1 contrast ratio — for normal text; 3:1 for large text and UI components.
- No information conveyed by color alone — always pair with text, icon, or pattern.
- Support Reduce Motion — gate animations behind
AccessibilityInfo.isReduceMotionEnabled().
Quick Reference: Core A11y Props
| Prop |
Platform |
Purpose |
accessible |
Both |
Groups children into one focusable node |
accessibilityLabel |
Both |
Name read aloud by screen reader |
accessibilityHint |
Both |
Explains the result of an action |
accessibilityRole |
Both |
Semantic role (button, link, header, image, etc.) |
accessibilityState |
Both |
{disabled, selected, checked, expanded, busy} |
accessibilityValue |
Both |
{min, max, now, text} for sliders/progress |
accessibilityLiveRegion |
Android |
'none', 'polite', 'assertive' for dynamic content |
aria-live |
Both (RN ≥0.73) |
ARIA-aligned live region |
accessibilityViewIsModal |
iOS |
Traps focus inside modal |
importantForAccessibility |
Android |
'yes', 'no', 'no-hide-descendants' |
onAccessibilityAction |
Both |
Custom actions for screen readers |
accessibilityActions |
Both |
Declares custom action names |
For detailed prop usage with examples, see references/props.md.
Implementation Workflow
Step 1 — Audit (find gaps)
Run the audit script to surface issues before writing any code:
node scripts/audit.js <path-to-src>
This checks for:
- Touchable/Pressable elements missing
accessibilityLabel or accessibilityRole
- Images missing
accessibilityLabel or accessibilityIgnoresInvertColors
TextInput missing accessibilityLabel (separate from placeholder)
- Custom interactive components lacking roles
- Potential color-only information (flag for manual review)
Then do a manual walkthrough with:
- iOS Simulator → Accessibility Inspector (
Xcode > Open Developer Tool > Accessibility Inspector)
- Android Emulator → TalkBack (Settings > Accessibility > TalkBack)
Step 2 — Fix by component type
See references/components.md for canonical patterns for:
- Buttons & Pressables
- Text Inputs & Forms
- Images & Icons
- Lists & FlatList
- Navigation (Tab bars, Stack headers)
- Modals & Bottom Sheets
- Custom controls (Switch, Slider, Checkbox, Radio)
- Loading / Skeleton states
Step 3 — Verify color contrast
- Extract all color pairs (text + background) from your design tokens / StyleSheet.
- Run the contrast check:
node scripts/contrast-check.js
- Fix any failures — see
references/contrast.md for replacement palette strategies.
Step 4 — Add automated testing
Install and configure:
npx expo install @testing-library/react-native
npm install --save-dev eslint-plugin-jsx-a11y
Add to .eslintrc:
{
"plugins": ["jsx-a11y"],
"extends": ["plugin:jsx-a11y/recommended"],
"rules": {
"jsx-a11y/interactive-supports-focus": "error"
}
}
Write accessibility-focused unit tests — see references/testing.md.
Step 5 — Reduce Motion & Dynamic Type
import { AccessibilityInfo } from 'react-native';
// Reduce Motion
const [reduceMotion, setReduceMotion] = useState(false);
useEffect(() => {
AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduceMotion);
return () => sub.remove();
}, []);
// Dynamic Type (iOS) — use allowFontScaling (default true, do NOT set false)
<Text allowFontScaling={true} maxFontSizeMultiplier={2}>...</Text>
Audit Checklist
Use this checklist per screen. Check each item; document failures with component name + line number.
Perceivable
Operable
Understandable
Robust
Common Mistakes to Avoid
- ❌
accessibilityLabel on a View wrapping a TouchableOpacity (set it on the Touchable itself)
- ❌ Using
placeholder as the only label for TextInput (VoiceOver reads placeholder as label, but it disappears on input)
- ❌
accessible={false} on a container that has interactive children (hides them from screen reader)
- ❌ Hardcoded colors that ignore dark mode / high-contrast mode
- ❌ Calling
AccessibilityInfo.announceForAccessibility on every render (debounce or trigger only on meaningful changes)
- ❌ Setting
importantForAccessibility="no-hide-descendants" on a visible interactive group
Expo-Specific Notes
- Managed Workflow: All standard RN a11y props work. No native module needed.
- expo-router: Navigation screen titles are announced automatically. Supplement with
Stack.Screen options={{ title }} for clarity.
- expo-image: Supports
accessibilityLabel prop directly; use role="presentation" for decorative images.
- expo-av / expo-video: Provide captions/subtitles via
subtitleStyles and tracks props. Announce play/pause state via accessibilityState={{ busy: isLoading }}.
- Expo Go: Accessibility Inspector works with Expo Go on physical devices.
Reference Files
| File |
Contents |
references/props.md |
Full prop API reference with before/after code examples |
references/components.md |
Canonical accessible patterns for 15+ component types |
references/contrast.md |
WCAG contrast ratios, formulas, palette fix strategies |
references/testing.md |
Unit test patterns, e2e with Detox, CI integration |
Scripts
| Script |
Purpose |
scripts/audit.js |
Static AST scan for missing a11y props |
scripts/contrast-check.js |
Extract and validate color contrast from JS/TS files |
Source: ucdavis/ai-skills-registry — distributed by TomeVault.
1---2name: accessibility-react-native3description: Implement, audit, and fix accessibility (a11y) in React Native and Expo mobile apps. Use when this capability is needed.4---56# React Native / Expo Accessibility Skill78This skill implements and audits accessibility (a11y) in React Native and Expo apps, following9WCAG 2.1 AA standards as adapted for mobile (iOS VoiceOver + Android TalkBack).1011## When This Skill Is Needed1213Use this skill for:14- Implementing `accessibilityLabel`, `accessibilityHint`, `accessibilityRole`, `accessibilityState`, `accessibilityValue`15- Auditing a component, screen, or entire app for a11y gaps16- Fixing VoiceOver / TalkBack focus order and grouping17- Checking color contrast and tap target sizes18- Writing accessible custom components (modals, carousels, date pickers, etc.)19- Integrating automated a11y testing (eslint-plugin-jsx-a11y adapted for RN, react-native-a11y)20- Adding `AccessibilityInfo` API usage (reduce motion, screen reader detection)2122---2324## Core Principles25261. **Every interactive element must have an accessible name** — via `accessibilityLabel` or meaningful text child.272. **Every interactive element must have a role** — `accessibilityRole` tells screen readers *what* it is.283. **State must be announced** — use `accessibilityState` for `disabled`, `selected`, `checked`, `expanded`, `busy`.294. **Logical reading order** — use `importantForAccessibility` (Android) and `accessibilityViewIsModal` to manage focus scope.305. **Tap targets ≥ 44×44 pts** — per Apple HIG and Android guidelines.316. **4.5:1 contrast ratio** — for normal text; 3:1 for large text and UI components.327. **No information conveyed by color alone** — always pair with text, icon, or pattern.338. **Support Reduce Motion** — gate animations behind `AccessibilityInfo.isReduceMotionEnabled()`.3435---3637## Quick Reference: Core A11y Props3839| Prop | Platform | Purpose |40|------|----------|---------|41| `accessible` | Both | Groups children into one focusable node |42| `accessibilityLabel` | Both | Name read aloud by screen reader |43| `accessibilityHint` | Both | Explains the result of an action |44| `accessibilityRole` | Both | Semantic role (button, link, header, image, etc.) |45| `accessibilityState` | Both | `{disabled, selected, checked, expanded, busy}` |46| `accessibilityValue` | Both | `{min, max, now, text}` for sliders/progress |47| `accessibilityLiveRegion` | Android | `'none'`, `'polite'`, `'assertive'` for dynamic content |48| `aria-live` | Both (RN ≥0.73) | ARIA-aligned live region |49| `accessibilityViewIsModal` | iOS | Traps focus inside modal |50| `importantForAccessibility` | Android | `'yes'`, `'no'`, `'no-hide-descendants'` |51| `onAccessibilityAction` | Both | Custom actions for screen readers |52| `accessibilityActions` | Both | Declares custom action names |5354For detailed prop usage with examples, see [`references/props.md`](references/props.md).5556---5758## Implementation Workflow5960### Step 1 — Audit (find gaps)6162Run the audit script to surface issues before writing any code:6364```bash65node scripts/audit.js <path-to-src>66```6768This checks for:69- Touchable/Pressable elements missing `accessibilityLabel` or `accessibilityRole`70- Images missing `accessibilityLabel` or `accessibilityIgnoresInvertColors`71- `TextInput` missing `accessibilityLabel` (separate from `placeholder`)72- Custom interactive components lacking roles73- Potential color-only information (flag for manual review)7475Then do a **manual walkthrough** with:76- iOS Simulator → Accessibility Inspector (`Xcode > Open Developer Tool > Accessibility Inspector`)77- Android Emulator → TalkBack (Settings > Accessibility > TalkBack)7879### Step 2 — Fix by component type8081See [`references/components.md`](references/components.md) for canonical patterns for:82- Buttons & Pressables83- Text Inputs & Forms84- Images & Icons85- Lists & FlatList86- Navigation (Tab bars, Stack headers)87- Modals & Bottom Sheets88- Custom controls (Switch, Slider, Checkbox, Radio)89- Loading / Skeleton states9091### Step 3 — Verify color contrast92931. Extract all color pairs (text + background) from your design tokens / StyleSheet.942. Run the contrast check: `node scripts/contrast-check.js`953. Fix any failures — see [`references/contrast.md`](references/contrast.md) for replacement palette strategies.9697### Step 4 — Add automated testing9899Install and configure:100```bash101npx expo install @testing-library/react-native102npm install --save-dev eslint-plugin-jsx-a11y103```104105Add to `.eslintrc`:106```json107{108 "plugins": ["jsx-a11y"],109 "extends": ["plugin:jsx-a11y/recommended"],110 "rules": {111 "jsx-a11y/interactive-supports-focus": "error"112 }113}114```115116Write accessibility-focused unit tests — see [`references/testing.md`](references/testing.md).117118### Step 5 — Reduce Motion & Dynamic Type119120```js121import { AccessibilityInfo } from 'react-native';122123// Reduce Motion124const [reduceMotion, setReduceMotion] = useState(false);125useEffect(() => {126 AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);127 const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduceMotion);128 return () => sub.remove();129}, []);130131// Dynamic Type (iOS) — use allowFontScaling (default true, do NOT set false)132<Text allowFontScaling={true} maxFontSizeMultiplier={2}>...</Text>133```134135---136137## Audit Checklist138139Use this checklist per screen. Check each item; document failures with component name + line number.140141### Perceivable142- [ ] All images have `accessibilityLabel` or are marked decorative (`accessible={false}`)143- [ ] Color is not the only means of conveying information144- [ ] Text contrast ≥ 4.5:1 (normal) / 3:1 (large ≥18pt or bold ≥14pt)145- [ ] UI component contrast ≥ 3:1 against adjacent colors146- [ ] `allowFontScaling` is NOT disabled on any `Text` or `TextInput`147148### Operable149- [ ] All interactive elements have `accessibilityRole`150- [ ] All interactive elements have `accessibilityLabel` (or unambiguous text child)151- [ ] Tap targets ≥ 44×44 pts (use `hitSlop` to extend without changing layout)152- [ ] Focus order matches reading/visual order153- [ ] No time limits without accessible pause/extend mechanism154- [ ] Animations respect Reduce Motion setting155156### Understandable157- [ ] Form inputs have visible labels (not just placeholder)158- [ ] Error messages are programmatically associated with inputs159- [ ] `accessibilityHint` used where action outcome isn't obvious160- [ ] `accessibilityLiveRegion` / `aria-live` on dynamic content (toasts, errors, countdowns)161162### Robust163- [ ] Custom interactive components announce state changes164- [ ] Modal/overlay traps focus (`accessibilityViewIsModal={true}` on iOS)165- [ ] Keyboard/switch access works (external keyboard, switch control)166- [ ] No a11y props on decorative non-interactive `View`s (reduces noise)167168---169170## Common Mistakes to Avoid171172- ❌ `accessibilityLabel` on a `View` wrapping a `TouchableOpacity` (set it on the Touchable itself)173- ❌ Using `placeholder` as the only label for `TextInput` (VoiceOver reads placeholder as label, but it disappears on input)174- ❌ `accessible={false}` on a container that has interactive children (hides them from screen reader)175- ❌ Hardcoded colors that ignore dark mode / high-contrast mode176- ❌ Calling `AccessibilityInfo.announceForAccessibility` on every render (debounce or trigger only on meaningful changes)177- ❌ Setting `importantForAccessibility="no-hide-descendants"` on a visible interactive group178179---180181## Expo-Specific Notes182183- **Managed Workflow**: All standard RN a11y props work. No native module needed.184- **expo-router**: Navigation screen titles are announced automatically. Supplement with `Stack.Screen options={{ title }}` for clarity.185- **expo-image**: Supports `accessibilityLabel` prop directly; use `role="presentation"` for decorative images.186- **expo-av / expo-video**: Provide captions/subtitles via `subtitleStyles` and `tracks` props. Announce play/pause state via `accessibilityState={{ busy: isLoading }}`.187- **Expo Go**: Accessibility Inspector works with Expo Go on physical devices.188189---190191## Reference Files192193| File | Contents |194|------|----------|195| [`references/props.md`](references/props.md) | Full prop API reference with before/after code examples |196| [`references/components.md`](references/components.md) | Canonical accessible patterns for 15+ component types |197| [`references/contrast.md`](references/contrast.md) | WCAG contrast ratios, formulas, palette fix strategies |198| [`references/testing.md`](references/testing.md) | Unit test patterns, e2e with Detox, CI integration |199200## Scripts201202| Script | Purpose |203|--------|---------|204| [`scripts/audit.js`](scripts/audit.js) | Static AST scan for missing a11y props |205| [`scripts/contrast-check.js`](scripts/contrast-check.js) | Extract and validate color contrast from JS/TS files |206207---208> Source: [ucdavis/ai-skills-registry](https://github.com/ucdavis/ai-skills-registry) — distributed by [TomeVault](https://tomevault.io).209<!-- tomevault:4.0:skill_md:2026-06-16 -->