Accessibility¶
Accessibility is a build requirement in this repo, not a later audit pass. CI fails on accessibility lint violations, and component tests query by role and label so tests break when the accessibility layer breaks.
Conformance target¶
WCAG 2.2 Level AA, applied to native mobile via the W3C mobile accessibility mapping. Because the app serves older adults, we treat large text support and touch target size as hard requirements rather than best effort.
Assistive technology test matrix¶
| Platform | Assistive technology | Cadence |
|---|---|---|
| iPhone | VoiceOver | Every release |
| iPad | VoiceOver | Every release |
| iPad | External keyboard (Tab/arrow navigation) | Every release |
| Android | TalkBack | Every release |
| iOS | Switch Control | Every release |
| Both | Largest platform font size (Dynamic Type / Android font scale) | Every PR that touches layout |
| Both | Dark mode | Every PR that touches color |
Build-time conventions¶
- Every touchable exposes
accessibilityRole, and anaccessibilityLabelwhen the visible text is absent or insufficient. Use the shared Pressable wrapper, which makes the role a required prop. - Minimum hit target is 44 by 44 points, enforced by the shared Pressable wrapper via the
minHitTargettoken, not left to call sites. - Never set
allowFontScaling={false}. Layouts must survive the largest Dynamic Type and Android font scale settings without clipping or overlap. - Nothing scrolls. Screens are built from the
Screenframe and fit the display window at both in-app text sizes; see the one-screen rule. Past that budget — OS accessibility text sizes on top of Extra large — the frame scrolls rather than clipping, because losing content at a large text size fails WCAG 1.4.4 (Resize Text). - Text styling goes through
ThemedText(orresolveTypeStylefor bare inputs), so the user's appearance settings — theme override, text size, font — reach every presentation of text. Do not hardcodefontSizeorfontFamilyin components; the in-app Extra large size composes with OS font scaling, never replaces it. - Respect reduced motion. Animated components use
use-reduced-motion.ts;
useMotionSafeDurationreturns 0 when the OS requests reduced motion so transitions snap instead of animating. - Color tokens in colors.ts carry measured contrast ratios against their intended backgrounds. If you change a color, recompute the ratio (WCAG 2.x relative luminance) and update the comment. Minimums: 4.5:1 for text, 3:1 for large text and UI components. Never encode meaning in color alone.
- Focus order follows visual order. Use
accessibilityViewIsModalon overlays and move focus to newly presented screens (AccessibilityInfo.announceForAccessibilityor focus APIs). - Form fields associate labels, errors, and hints programmatically
(
accessibilityLabel,accessibilityHint,accessibilityState), and validation errors are announced rather than signaled by color alone. See the switch row in settings-screen.tsx for the pattern.
Automated checks¶
eslint-plugin-react-native-a11yruns with its strictest preset via eslint.config.js. CI runspnpm lint --max-warnings 0, so violations fail the build.- Tests use React Native Testing Library with
getByRole/getByLabelTextqueries by preference (see tests/).
Manual pass (before each release)¶
- VoiceOver on iPhone and iPad: every screen reads in a sensible order, no unlabeled controls, state changes are announced.
- TalkBack on Android: same pass as VoiceOver.
- External keyboard on iPad: all interactive elements reachable and operable, visible focus, no traps.
- Switch Control: primary flows completable.
- Largest Dynamic Type and Android font scale: no clipped or overlapping text.
- iPad Split View at the narrowest width and dark mode: layouts and contrast hold up.