0
# Accessibility
1
2
React Native's accessibility API adapted for web with comprehensive support for screen readers, reduced motion preferences, focus management, and WCAG-compliant accessibility features.
3
4
## AccessibilityInfo
5
6
Accessibility information API providing methods to query accessibility settings, manage focus, and communicate with assistive technologies on web platforms.
7
8
```javascript { .api }
9
const AccessibilityInfo: {
10
isScreenReaderEnabled: () => Promise<boolean>;
11
isReduceMotionEnabled: () => Promise<boolean>;
12
addEventListener: (eventName: string, handler: Function) => { remove: () => void };
13
removeEventListener: (eventName: string, handler: Function) => void;
14
setAccessibilityFocus: (reactTag: number) => void;
15
announceForAccessibility: (announcement: string) => void;
16
fetch: () => Promise<boolean>; // deprecated
17
};
18
```
19
20
### isScreenReaderEnabled()
21
Query whether a screen reader is currently enabled (always returns true on web).
22
23
```javascript { .api }
24
AccessibilityInfo.isScreenReaderEnabled(): Promise<boolean>
25
```
26
27
### isReduceMotionEnabled()
28
Query whether the user prefers reduced motion based on system settings.
29
30
```javascript { .api }
31
AccessibilityInfo.isReduceMotionEnabled(): Promise<boolean>
32
```
33
34
### addEventListener()
35
Add event listeners for accessibility-related changes.
36
37
```javascript { .api }
38
AccessibilityInfo.addEventListener(
39
eventName: 'reduceMotionChanged' | 'screenReaderChanged',
40
handler: (enabled: boolean) => void
41
): { remove: () => void }
42
```
43
44
### setAccessibilityFocus()
45
Set accessibility focus to a specific element (web implementation is no-op).
46
47
```javascript { .api }
48
AccessibilityInfo.setAccessibilityFocus(reactTag: number): void
49
```
50
51
### announceForAccessibility()
52
Announce a message to screen readers (web implementation is no-op, use aria-live regions instead).
53
54
```javascript { .api }
55
AccessibilityInfo.announceForAccessibility(announcement: string): void
56
```
57
58
**Usage:**
59
```javascript
60
import { AccessibilityInfo } from "react-native-web";
61
62
function AccessibilityDemo() {
63
const [isScreenReaderEnabled, setIsScreenReaderEnabled] = React.useState(false);
64
const [isReduceMotionEnabled, setIsReduceMotionEnabled] = React.useState(false);
65
const [announcements, setAnnouncements] = React.useState([]);
66
67
React.useEffect(() => {
68
// Query initial accessibility settings
69
AccessibilityInfo.isScreenReaderEnabled().then(setIsScreenReaderEnabled);
70
AccessibilityInfo.isReduceMotionEnabled().then(setIsReduceMotionEnabled);
71
72
// Listen for reduced motion changes
73
const subscription = AccessibilityInfo.addEventListener(
74
'reduceMotionChanged',
75
(enabled) => {
76
setIsReduceMotionEnabled(enabled);
77
console.log('Reduced motion preference changed:', enabled);
78
}
79
);
80
81
return () => subscription.remove();
82
}, []);
83
84
const announceToScreenReader = (message) => {
85
// Web-specific implementation using aria-live regions
86
setAnnouncements(prev => [...prev, { id: Date.now(), message }]);
87
88
// Also call the API (no-op on web, but maintains compatibility)
89
AccessibilityInfo.announceForAccessibility(message);
90
91
// Remove announcement after it's been read
92
setTimeout(() => {
93
setAnnouncements(prev => prev.filter(a => a.message !== message));
94
}, 3000);
95
};
96
97
const handleButtonPress = () => {
98
announceToScreenReader('Button was pressed successfully');
99
};
100
101
return (
102
<View style={styles.container}>
103
<Text style={styles.title}>Accessibility Information</Text>
104
105
{/* Screen reader status */}
106
<View style={styles.statusContainer}>
107
<Text style={styles.label}>Screen Reader:</Text>
108
<Text style={styles.value}>
109
{isScreenReaderEnabled ? 'Enabled' : 'Disabled'}
110
</Text>
111
</View>
112
113
{/* Reduced motion status */}
114
<View style={styles.statusContainer}>
115
<Text style={styles.label}>Reduced Motion:</Text>
116
<Text style={styles.value}>
117
{isReduceMotionEnabled ? 'Preferred' : 'Not Preferred'}
118
</Text>
119
</View>
120
121
{/* Interactive elements */}
122
<TouchableOpacity
123
style={styles.button}
124
onPress={handleButtonPress}
125
accessibilityRole="button"
126
accessibilityLabel="Announce test button"
127
accessibilityHint="Tap to test screen reader announcement"
128
>
129
<Text style={styles.buttonText}>Test Announcement</Text>
130
</TouchableOpacity>
131
132
<TouchableOpacity
133
style={styles.button}
134
onPress={() => announceToScreenReader('This is a custom announcement')}
135
accessibilityRole="button"
136
accessibilityLabel="Custom announcement button"
137
>
138
<Text style={styles.buttonText}>Custom Announcement</Text>
139
</TouchableOpacity>
140
141
{/* Aria-live region for announcements */}
142
<View
143
style={styles.announcements}
144
aria-live="polite"
145
aria-atomic="true"
146
>
147
{announcements.map(announcement => (
148
<Text
149
key={announcement.id}
150
style={styles.srOnly}
151
>
152
{announcement.message}
153
</Text>
154
))}
155
</View>
156
</View>
157
);
158
}
159
160
// Enhanced accessibility utilities
161
class WebAccessibilityManager {
162
static mediaQuery = null;
163
static listeners = new Set();
164
165
static init() {
166
if (typeof window !== 'undefined' && !this.mediaQuery) {
167
this.mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
168
this.mediaQuery.addEventListener('change', this.handleMediaChange);
169
}
170
}
171
172
static handleMediaChange = (event) => {
173
this.listeners.forEach(callback => {
174
callback(event.matches);
175
});
176
};
177
178
static addReducedMotionListener(callback) {
179
this.init();
180
this.listeners.add(callback);
181
182
// Call immediately with current value
183
if (this.mediaQuery) {
184
callback(this.mediaQuery.matches);
185
}
186
187
return {
188
remove: () => {
189
this.listeners.delete(callback);
190
}
191
};
192
}
193
194
static async getReducedMotionPreference() {
195
this.init();
196
return this.mediaQuery ? this.mediaQuery.matches : false;
197
}
198
199
// Focus management
200
static setFocus(element) {
201
if (element && typeof element.focus === 'function') {
202
element.focus();
203
return true;
204
}
205
return false;
206
}
207
208
static trapFocus(container) {
209
const focusableElements = container.querySelectorAll(
210
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
211
);
212
213
if (focusableElements.length === 0) return { release: () => {} };
214
215
const firstElement = focusableElements[0];
216
const lastElement = focusableElements[focusableElements.length - 1];
217
218
const handleTabKey = (e) => {
219
if (e.key !== 'Tab') return;
220
221
if (e.shiftKey) {
222
if (document.activeElement === firstElement) {
223
e.preventDefault();
224
lastElement.focus();
225
}
226
} else {
227
if (document.activeElement === lastElement) {
228
e.preventDefault();
229
firstElement.focus();
230
}
231
}
232
};
233
234
container.addEventListener('keydown', handleTabKey);
235
firstElement.focus();
236
237
return {
238
release: () => {
239
container.removeEventListener('keydown', handleTabKey);
240
}
241
};
242
}
243
244
// Screen reader announcements
245
static createAnnouncementRegion() {
246
let region = document.getElementById('accessibility-announcements');
247
248
if (!region) {
249
region = document.createElement('div');
250
region.id = 'accessibility-announcements';
251
region.setAttribute('aria-live', 'polite');
252
region.setAttribute('aria-atomic', 'true');
253
region.style.position = 'absolute';
254
region.style.left = '-10000px';
255
region.style.width = '1px';
256
region.style.height = '1px';
257
region.style.overflow = 'hidden';
258
document.body.appendChild(region);
259
}
260
261
return region;
262
}
263
264
static announce(message, priority = 'polite') {
265
const region = this.createAnnouncementRegion();
266
region.setAttribute('aria-live', priority);
267
268
// Clear and set new message
269
region.textContent = '';
270
setTimeout(() => {
271
region.textContent = message;
272
}, 100);
273
274
// Clear after announcement
275
setTimeout(() => {
276
region.textContent = '';
277
}, 3000);
278
}
279
280
// High contrast detection
281
static detectHighContrast() {
282
if (typeof window === 'undefined') return false;
283
284
// Check for Windows high contrast mode
285
const testDiv = document.createElement('div');
286
testDiv.style.backgroundImage = 'url(data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==)';
287
testDiv.style.position = 'absolute';
288
testDiv.style.left = '-9999px';
289
document.body.appendChild(testDiv);
290
291
const hasBackgroundImage = window.getComputedStyle(testDiv).backgroundImage !== 'none';
292
document.body.removeChild(testDiv);
293
294
return !hasBackgroundImage;
295
}
296
297
// Color scheme preference
298
static getColorSchemePreference() {
299
if (typeof window === 'undefined') return 'light';
300
301
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
302
return darkModeQuery.matches ? 'dark' : 'light';
303
}
304
}
305
306
// Accessible component examples
307
function AccessibleModal({ visible, onClose, title, children }) {
308
const modalRef = React.useRef(null);
309
const [focusTrap, setFocusTrap] = React.useState(null);
310
311
React.useEffect(() => {
312
if (visible && modalRef.current) {
313
const trap = WebAccessibilityManager.trapFocus(modalRef.current);
314
setFocusTrap(trap);
315
316
// Announce modal opening
317
WebAccessibilityManager.announce(`${title} dialog opened`);
318
319
return () => {
320
trap.release();
321
WebAccessibilityManager.announce('Dialog closed');
322
};
323
}
324
}, [visible, title]);
325
326
if (!visible) return null;
327
328
return (
329
<View
330
style={styles.modalOverlay}
331
accessibilityRole="dialog"
332
accessibilityModal={true}
333
accessibilityLabel={title}
334
>
335
<View
336
ref={modalRef}
337
style={styles.modalContent}
338
accessibilityViewIsModal={true}
339
>
340
<View style={styles.modalHeader}>
341
<Text style={styles.modalTitle} accessibilityRole="heading">
342
{title}
343
</Text>
344
<TouchableOpacity
345
onPress={onClose}
346
style={styles.closeButton}
347
accessibilityRole="button"
348
accessibilityLabel="Close dialog"
349
>
350
<Text>×</Text>
351
</TouchableOpacity>
352
</View>
353
354
<View style={styles.modalBody}>
355
{children}
356
</View>
357
</View>
358
</View>
359
);
360
}
361
362
function AccessibleForm() {
363
const [formData, setFormData] = React.useState({
364
name: '',
365
email: '',
366
message: ''
367
});
368
const [errors, setErrors] = React.useState({});
369
370
const validateForm = () => {
371
const newErrors = {};
372
373
if (!formData.name.trim()) {
374
newErrors.name = 'Name is required';
375
}
376
377
if (!formData.email.trim()) {
378
newErrors.email = 'Email is required';
379
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
380
newErrors.email = 'Email is invalid';
381
}
382
383
setErrors(newErrors);
384
385
if (Object.keys(newErrors).length > 0) {
386
WebAccessibilityManager.announce('Form has errors. Please review and correct.');
387
return false;
388
}
389
390
return true;
391
};
392
393
const handleSubmit = () => {
394
if (validateForm()) {
395
WebAccessibilityManager.announce('Form submitted successfully');
396
// Handle form submission
397
}
398
};
399
400
return (
401
<View style={styles.form}>
402
<Text style={styles.formTitle} accessibilityRole="heading">
403
Contact Form
404
</Text>
405
406
<View style={styles.fieldGroup}>
407
<Text style={styles.label}>
408
Name *
409
</Text>
410
<TextInput
411
style={[styles.input, errors.name && styles.inputError]}
412
value={formData.name}
413
onChangeText={(name) => setFormData(prev => ({ ...prev, name }))}
414
accessibilityLabel="Name"
415
accessibilityRequired={true}
416
accessibilityInvalid={!!errors.name}
417
accessibilityErrorMessage={errors.name}
418
/>
419
{errors.name && (
420
<Text style={styles.errorText} accessibilityRole="alert">
421
{errors.name}
422
</Text>
423
)}
424
</View>
425
426
<View style={styles.fieldGroup}>
427
<Text style={styles.label}>
428
Email *
429
</Text>
430
<TextInput
431
style={[styles.input, errors.email && styles.inputError]}
432
value={formData.email}
433
onChangeText={(email) => setFormData(prev => ({ ...prev, email }))}
434
keyboardType="email-address"
435
accessibilityLabel="Email address"
436
accessibilityRequired={true}
437
accessibilityInvalid={!!errors.email}
438
accessibilityErrorMessage={errors.email}
439
/>
440
{errors.email && (
441
<Text style={styles.errorText} accessibilityRole="alert">
442
{errors.email}
443
</Text>
444
)}
445
</View>
446
447
<View style={styles.fieldGroup}>
448
<Text style={styles.label}>
449
Message
450
</Text>
451
<TextInput
452
style={[styles.input, styles.textArea]}
453
value={formData.message}
454
onChangeText={(message) => setFormData(prev => ({ ...prev, message }))}
455
multiline={true}
456
numberOfLines={4}
457
accessibilityLabel="Message"
458
accessibilityHint="Optional message or comments"
459
/>
460
</View>
461
462
<TouchableOpacity
463
style={styles.submitButton}
464
onPress={handleSubmit}
465
accessibilityRole="button"
466
accessibilityLabel="Submit contact form"
467
>
468
<Text style={styles.submitButtonText}>Submit</Text>
469
</TouchableOpacity>
470
</View>
471
);
472
}
473
474
// Motion-aware animation wrapper
475
function MotionAwareAnimation({ children, animation, reducedMotionFallback }) {
476
const [prefersReducedMotion, setPrefersReducedMotion] = React.useState(false);
477
478
React.useEffect(() => {
479
const subscription = WebAccessibilityManager.addReducedMotionListener(
480
setPrefersReducedMotion
481
);
482
return subscription.remove;
483
}, []);
484
485
const animationToUse = prefersReducedMotion
486
? (reducedMotionFallback || { duration: 0 })
487
: animation;
488
489
return children(animationToUse);
490
}
491
492
const styles = StyleSheet.create({
493
container: {
494
flex: 1,
495
padding: 20,
496
backgroundColor: '#fff'
497
},
498
title: {
499
fontSize: 24,
500
fontWeight: 'bold',
501
marginBottom: 20,
502
accessibilityRole: 'heading'
503
},
504
statusContainer: {
505
flexDirection: 'row',
506
marginBottom: 10,
507
alignItems: 'center'
508
},
509
label: {
510
fontWeight: 'bold',
511
marginRight: 8,
512
minWidth: 120
513
},
514
value: {
515
flex: 1
516
},
517
button: {
518
backgroundColor: '#007AFF',
519
padding: 12,
520
borderRadius: 8,
521
marginVertical: 8,
522
alignItems: 'center'
523
},
524
buttonText: {
525
color: 'white',
526
fontWeight: '600'
527
},
528
announcements: {
529
position: 'absolute',
530
left: -10000,
531
width: 1,
532
height: 1,
533
overflow: 'hidden'
534
},
535
srOnly: {
536
position: 'absolute',
537
left: -10000,
538
width: 1,
539
height: 1,
540
overflow: 'hidden'
541
},
542
543
// Modal styles
544
modalOverlay: {
545
position: 'absolute',
546
top: 0,
547
left: 0,
548
right: 0,
549
bottom: 0,
550
backgroundColor: 'rgba(0,0,0,0.5)',
551
justifyContent: 'center',
552
alignItems: 'center'
553
},
554
modalContent: {
555
backgroundColor: 'white',
556
borderRadius: 12,
557
padding: 20,
558
minWidth: 300,
559
maxWidth: 500,
560
maxHeight: '80%'
561
},
562
modalHeader: {
563
flexDirection: 'row',
564
justifyContent: 'space-between',
565
alignItems: 'center',
566
marginBottom: 16
567
},
568
modalTitle: {
569
fontSize: 18,
570
fontWeight: 'bold'
571
},
572
closeButton: {
573
padding: 8,
574
borderRadius: 4
575
},
576
modalBody: {
577
flex: 1
578
},
579
580
// Form styles
581
form: {
582
padding: 20
583
},
584
formTitle: {
585
fontSize: 20,
586
fontWeight: 'bold',
587
marginBottom: 20
588
},
589
fieldGroup: {
590
marginBottom: 16
591
},
592
input: {
593
borderWidth: 1,
594
borderColor: '#ccc',
595
borderRadius: 4,
596
padding: 12,
597
fontSize: 16
598
},
599
inputError: {
600
borderColor: '#ff0000'
601
},
602
textArea: {
603
height: 100,
604
textAlignVertical: 'top'
605
},
606
errorText: {
607
color: '#ff0000',
608
fontSize: 14,
609
marginTop: 4
610
},
611
submitButton: {
612
backgroundColor: '#28a745',
613
padding: 16,
614
borderRadius: 8,
615
alignItems: 'center',
616
marginTop: 20
617
},
618
submitButtonText: {
619
color: 'white',
620
fontSize: 16,
621
fontWeight: '600'
622
}
623
});
624
```
625
626
## Web-Specific Implementation
627
628
React Native Web's AccessibilityInfo implementation leverages web standards and browser APIs to provide comprehensive accessibility support:
629
630
**Key Features:**
631
- **Media Query Integration**: Uses `prefers-reduced-motion` CSS media query for motion preferences
632
- **ARIA Support**: Provides utilities for ARIA live regions, roles, and properties
633
- **Focus Management**: Implements focus trapping and programmatic focus control
634
- **Screen Reader Compatibility**: Works with NVDA, JAWS, VoiceOver, and other assistive technologies
635
- **High Contrast Detection**: Detects Windows high contrast mode and other accessibility preferences
636
637
**Implementation Details:**
638
- `isScreenReaderEnabled()` always returns `true` as web assumes screen reader availability
639
- `isReduceMotionEnabled()` uses `(prefers-reduced-motion: reduce)` media query
640
- `announceForAccessibility()` requires custom ARIA live region implementation
641
- Events are based on CSS media query change listeners
642
- Focus management uses native DOM APIs
643
644
**Best Practices:**
645
- Always provide alternative text for images using `accessibilityLabel`
646
- Use semantic roles (`accessibilityRole`) for proper element identification
647
- Implement focus management for modals and dynamic content
648
- Respect `prefers-reduced-motion` for animations
649
- Provide error messages and validation feedback
650
- Use sufficient color contrast ratios (minimum 4.5:1)
651
- Ensure keyboard navigation works throughout the application
652
653
## Types
654
655
```javascript { .api }
656
interface AccessibilityInfoStatic {
657
isScreenReaderEnabled(): Promise<boolean>;
658
isReduceMotionEnabled(): Promise<boolean>;
659
addEventListener(
660
eventName: 'reduceMotionChanged' | 'screenReaderChanged',
661
handler: (enabled: boolean) => void
662
): { remove(): void };
663
removeEventListener(eventName: string, handler: Function): void;
664
setAccessibilityFocus(reactTag: number): void;
665
announceForAccessibility(announcement: string): void;
666
fetch(): Promise<boolean>; // deprecated
667
}
668
669
type AccessibilityEventHandler = (enabled: boolean) => void;
670
671
interface AccessibilitySubscription {
672
remove(): void;
673
}
674
675
interface FocusTrap {
676
release(): void;
677
}
678
```