Phase 1: Baseline Auditing Protocol
Do not rely on automated scanners alone. Screen readers interpret the accessibility tree, not the visual DOM. Use native tools to validate real-world navigation:
- iOS: Enable VoiceOver via Settings > Accessibility > VoiceOver. Navigate using the rotor to jump by headings, buttons, and links.
- Android: Enable TalkBack via Settings > Accessibility > TalkBack. Use two-finger swipe to scroll, single tap to select, double-tap to activate.
Validate the complete order flow: menu selection β customization β cart β payment β confirmation. Document every instance where focus is lost, labels are missing, or state changes are silent.
The following patterns replace common failure points with accessible equivalents.
1. Semantic Menu Cards
Custom views often strip accessibility metadata. Wrap interactive elements with explicit roles and labels.
import { View, Text, Pressable, StyleSheet } from 'react-native';
import type { AccessibilityProps } from 'react-native';
interface MenuItemProps extends AccessibilityProps {
name: string;
price: string;
defaultSize: string;
onPress: () => void;
}
export const AccessibleMenuCard = ({ name, price, defaultSize, onPress, ...rest }: MenuItemProps) => {
const combinedLabel = `${name}, ${defaultSize}, ${price}`;
return (
<Pressable
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={combinedLabel}
accessibilityHint="Double tap to select this item"
style={styles.card}
{...rest}
>
<Text style={styles.name}>{name}</Text>
<Text style={styles.price}>{price}</Text>
</Pressable>
);
};
const styles = StyleSheet.create({
card: { padding: 16, borderRadius: 8, backgroundColor: '#f8f9fa' },
name: { fontSize: 16, fontWeight: '600' },
price: { fontSize: 14, color: '#6c757d' }
});
Why this works: accessibilityRole="button" ensures the screen reader announces the element as interactive. accessibilityLabel provides the spoken equivalent of the visual content. accessibilityHint guides users unfamiliar with double-tap activation.
2. Radio Group Customization
Horizontal carousels break linear screen reader navigation. Replace with native radio semantics.
import { useState } from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';
interface Option { id: string; label: string; priceDelta: number; }
interface CustomizationPickerProps {
options: Option[];
selectedId: string;
onSelect: (id: string) => void;
}
export const AccessibleCustomizationPicker = ({ options, selectedId, onSelect }: CustomizationPickerProps) => {
return (
<View accessibilityRole="radiogroup" accessibilityLabel="Size selection">
{options.map((opt) => (
<Pressable
key={opt.id}
onPress={() => onSelect(opt.id)}
accessibilityRole="radio"
accessibilityState={{ checked: selectedId === opt.id }}
accessibilityLabel={`${opt.label}, ${opt.priceDelta === 0 ? 'no extra charge' : `$${opt.priceDelta.toFixed(2)} extra`}`}
style={[styles.option, selectedId === opt.id && styles.selected]}
>
<Text>{opt.label}</Text>
</Pressable>
))}
</View>
);
};
const styles = StyleSheet.create({
option: { padding: 12, borderWidth: 1, borderColor: '#dee2e6', borderRadius: 6, marginVertical: 4 },
selected: { borderColor: '#0d6efd', backgroundColor: '#e7f1ff' }
});
Why this works: accessibilityRole="radiogroup" and accessibilityRole="radio" create a logical grouping. accessibilityState={{ checked }} dynamically announces selection state. Screen readers now navigate linearly and announce price deltas without requiring visual scanning.
3. Focus Restoration After Payment
Biometric payment modals (Apple Pay, Google Pay) remove the app from the foreground. Focus must be explicitly restored to the confirmation screen.
import { useRef } from 'react';
import { Pressable, AccessibilityInfo, StyleSheet } from 'react-native';
export const PaymentButton = ({ onPaymentSuccess }: { onPaymentSuccess: () => void }) => {
const confirmationRef = useRef<Pressable>(null);
const handlePayment = async () => {
// Simulate payment gateway invocation
await processPayment();
// Restore focus and announce state
confirmationRef.current?.focus();
AccessibilityInfo.announceForAccessibility('Order confirmed. Redirecting to receipt.');
onPaymentSuccess();
};
return (
<Pressable onPress={handlePayment} style={styles.btn}>
<Text>Pay Now</Text>
</Pressable>
);
};
Why this works: AccessibilityInfo.announceForAccessibility pushes a live region update to VoiceOver/TalkBack. Explicit focus restoration prevents the "silent screen" phenomenon where users cannot determine transaction status.
Phase 3: Vendor Compliance Verification
Request the vendor's VPAT (Voluntary Product Accessibility Template) or ACR (Accessibility Conformance Report). Verify it aligns with WCAG 2.1 AA. If the vendor cannot produce documentation, initiate contract renegotiation or platform migration. Document all support tickets requesting accessibility fixes; these serve as evidence of good-faith remediation efforts in legal proceedings.
Pitfall Guide
1. The Visual Parity Fallacy
Explanation: Assuming that if an element looks correct on screen, it is accessible. Screen readers parse the accessibility tree, not visual layout. A styled View with text inside is invisible to assistive technology unless explicitly labeled.
Fix: Always pair visual components with accessibilityLabel, accessibilityRole, and accessibilityHint. Use react-native-testing-library's getByLabelText and getByRole queries in unit tests to validate tree structure.
2. Custom Gesture Overlays Without Fallbacks
Explanation: Swipe-to-dismiss, pinch-to-zoom, or drag-to-reorder interactions often lack keyboard or screen reader equivalents. VoiceOver users cannot perform multi-finger gestures reliably.
Fix: Implement standard tap interactions alongside gestures. Use accessibilityActions to expose custom gestures as standard screen reader commands. Provide explicit "Delete" or "Move" buttons as fallbacks.
3. Image-Only Data Representation
Explanation: Loyalty balances, tier badges, and promotional banners rendered as static images contain no machine-readable text. Screen readers announce "image" or skip the element entirely.
Fix: Render data as text nodes with visual styling layered via CSS/StyleSheet. If images are mandatory, attach accessibilityLabel with the exact data value. Mark decorative images with accessibilityIgnoresInvertColors or importantForAccessibility="noHideDescendants".
4. Focus Loss After Modal Dismissal
Explanation: Closing a customization drawer or payment modal often leaves focus on a detached element or the root view. Users lose their place in the navigation flow.
Fix: Store the previously focused element before opening a modal. Restore focus explicitly on close using AccessibilityInfo.setAccessibilityFocus() or component refs. Validate focus restoration in both iOS and Android simulators.
5. Ignoring Dynamic Type and Scaling
Explanation: Hardcoded font sizes and fixed container dimensions break when users enable Large Text or Dynamic Type. Content truncates, buttons overlap, and navigation becomes impossible.
Fix: Use relative units (rem, sp, or PixelRatio scaling). Implement adjustsFontForContentSizeCategory on iOS and android:scaleType="fitCenter" on text views. Test with maximum accessibility text sizes enabled.
6. Vendor Contract Gaps
Explanation: Standard SaaS agreements rarely include accessibility SLAs or indemnification clauses. When the platform fails compliance, the merchant bears full liability.
Fix: Add an accessibility addendum requiring WCAG 2.1 AA compliance, VPAT delivery, and 30-day remediation windows for reported defects. Include indemnification language shifting liability for platform-level failures back to the vendor.
7. Static Audit Reliance
Explanation: Running a single accessibility audit and assuming ongoing compliance. Mobile apps receive frequent updates, third-party SDK integrations, and dynamic content changes that introduce regressions.
Fix: Integrate accessibility linting (eslint-plugin-jsx-a11y, react-native-a11y) into CI/CD. Run automated screen reader simulation tests on every PR. Schedule quarterly manual audits with actual assistive technology users.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| SaaS Platform (Toast, Square, Clover) | Vendor VPAT request + contract addendum + documented support tickets | Platform controls the codebase; merchant lacks direct remediation access | Low upfront, shifts liability to vendor SLA |
| Custom-Built App | Accessibility-first component library + CI linting + quarterly manual audits | Full engineering control allows structural compliance | Moderate upfront, reduces long-term legal exposure |
| Hybrid (Custom UI + Third-Party Payment) | Isolate payment flow focus management + validate third-party SDK accessibility | Payment gateways often break focus trees; requires explicit restoration | Low, prevents high-risk transaction failures |
| Multi-Location Chain | Centralized accessibility policy + vendor standardization + automated monitoring | Consistent compliance across locations reduces class action risk | High initial, scales efficiently |
Configuration Template
{
"accessibilityAudit": {
"wcagVersion": "2.1",
"conformanceLevel": "AA",
"requiredChecks": [
"semanticRoles",
"labelCoverage",
"focusManagement",
"dynamicTypeSupport",
"contrastRatio",
"touchTargetSize"
],
"ciIntegration": {
"linter": "eslint-plugin-jsx-a11y",
"testingLibrary": "react-native-testing-library",
"screenReaderSimulation": "accessibility-insights-mobile"
},
"vendorRequirements": {
"documentType": "VPAT_2_4",
"remediationSLA": "30_days",
"indemnification": true
}
}
}
Quick Start Guide
- Enable Screen Reader: Open device Settings > Accessibility. Turn on VoiceOver (iOS) or TalkBack (Android).
- Navigate Blindfolded: Open your ordering app. Close your eyes or cover the screen. Attempt to place a standard order using only audio feedback.
- Log Failures: Record every instance where focus disappears, labels are missing, or state changes are silent. Export screenshots and timestamps.
- Apply Fixes: Implement semantic roles, radio groups, and focus restoration using the patterns above. Validate changes with the same screen reader workflow.
- Verify & Document: Run a second blind navigation pass. Save results, update your accessibility statement, and file vendor tickets if platform-level defects persist.