React Native for Web is a comprehensive compatibility library that enables React Native components and APIs to run seamlessly on web browsers using React DOM.
—
React Native's accessibility API adapted for web with comprehensive support for screen readers, reduced motion preferences, focus management, and WCAG-compliant accessibility features.
Accessibility information API providing methods to query accessibility settings, manage focus, and communicate with assistive technologies on web platforms.
const AccessibilityInfo: {
isScreenReaderEnabled: () => Promise<boolean>;
isReduceMotionEnabled: () => Promise<boolean>;
addEventListener: (eventName: string, handler: Function) => { remove: () => void };
removeEventListener: (eventName: string, handler: Function) => void;
setAccessibilityFocus: (reactTag: number) => void;
announceForAccessibility: (announcement: string) => void;
fetch: () => Promise<boolean>; // deprecated
};Query whether a screen reader is currently enabled (always returns true on web).
AccessibilityInfo.isScreenReaderEnabled(): Promise<boolean>Query whether the user prefers reduced motion based on system settings.
AccessibilityInfo.isReduceMotionEnabled(): Promise<boolean>Add event listeners for accessibility-related changes.
AccessibilityInfo.addEventListener(
eventName: 'reduceMotionChanged' | 'screenReaderChanged',
handler: (enabled: boolean) => void
): { remove: () => void }Set accessibility focus to a specific element (web implementation is no-op).
AccessibilityInfo.setAccessibilityFocus(reactTag: number): voidAnnounce a message to screen readers (web implementation is no-op, use aria-live regions instead).
AccessibilityInfo.announceForAccessibility(announcement: string): voidUsage:
import { AccessibilityInfo } from "react-native-web";
function AccessibilityDemo() {
const [isScreenReaderEnabled, setIsScreenReaderEnabled] = React.useState(false);
const [isReduceMotionEnabled, setIsReduceMotionEnabled] = React.useState(false);
const [announcements, setAnnouncements] = React.useState([]);
React.useEffect(() => {
// Query initial accessibility settings
AccessibilityInfo.isScreenReaderEnabled().then(setIsScreenReaderEnabled);
AccessibilityInfo.isReduceMotionEnabled().then(setIsReduceMotionEnabled);
// Listen for reduced motion changes
const subscription = AccessibilityInfo.addEventListener(
'reduceMotionChanged',
(enabled) => {
setIsReduceMotionEnabled(enabled);
console.log('Reduced motion preference changed:', enabled);
}
);
return () => subscription.remove();
}, []);
const announceToScreenReader = (message) => {
// Web-specific implementation using aria-live regions
setAnnouncements(prev => [...prev, { id: Date.now(), message }]);
// Also call the API (no-op on web, but maintains compatibility)
AccessibilityInfo.announceForAccessibility(message);
// Remove announcement after it's been read
setTimeout(() => {
setAnnouncements(prev => prev.filter(a => a.message !== message));
}, 3000);
};
const handleButtonPress = () => {
announceToScreenReader('Button was pressed successfully');
};
return (
<View style={styles.container}>
<Text style={styles.title}>Accessibility Information</Text>
{/* Screen reader status */}
<View style={styles.statusContainer}>
<Text style={styles.label}>Screen Reader:</Text>
<Text style={styles.value}>
{isScreenReaderEnabled ? 'Enabled' : 'Disabled'}
</Text>
</View>
{/* Reduced motion status */}
<View style={styles.statusContainer}>
<Text style={styles.label}>Reduced Motion:</Text>
<Text style={styles.value}>
{isReduceMotionEnabled ? 'Preferred' : 'Not Preferred'}
</Text>
</View>
{/* Interactive elements */}
<TouchableOpacity
style={styles.button}
onPress={handleButtonPress}
accessibilityRole="button"
accessibilityLabel="Announce test button"
accessibilityHint="Tap to test screen reader announcement"
>
<Text style={styles.buttonText}>Test Announcement</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.button}
onPress={() => announceToScreenReader('This is a custom announcement')}
accessibilityRole="button"
accessibilityLabel="Custom announcement button"
>
<Text style={styles.buttonText}>Custom Announcement</Text>
</TouchableOpacity>
{/* Aria-live region for announcements */}
<View
style={styles.announcements}
aria-live="polite"
aria-atomic="true"
>
{announcements.map(announcement => (
<Text
key={announcement.id}
style={styles.srOnly}
>
{announcement.message}
</Text>
))}
</View>
</View>
);
}
// Enhanced accessibility utilities
class WebAccessibilityManager {
static mediaQuery = null;
static listeners = new Set();
static init() {
if (typeof window !== 'undefined' && !this.mediaQuery) {
this.mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
this.mediaQuery.addEventListener('change', this.handleMediaChange);
}
}
static handleMediaChange = (event) => {
this.listeners.forEach(callback => {
callback(event.matches);
});
};
static addReducedMotionListener(callback) {
this.init();
this.listeners.add(callback);
// Call immediately with current value
if (this.mediaQuery) {
callback(this.mediaQuery.matches);
}
return {
remove: () => {
this.listeners.delete(callback);
}
};
}
static async getReducedMotionPreference() {
this.init();
return this.mediaQuery ? this.mediaQuery.matches : false;
}
// Focus management
static setFocus(element) {
if (element && typeof element.focus === 'function') {
element.focus();
return true;
}
return false;
}
static trapFocus(container) {
const focusableElements = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length === 0) return { release: () => {} };
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const handleTabKey = (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
};
container.addEventListener('keydown', handleTabKey);
firstElement.focus();
return {
release: () => {
container.removeEventListener('keydown', handleTabKey);
}
};
}
// Screen reader announcements
static createAnnouncementRegion() {
let region = document.getElementById('accessibility-announcements');
if (!region) {
region = document.createElement('div');
region.id = 'accessibility-announcements';
region.setAttribute('aria-live', 'polite');
region.setAttribute('aria-atomic', 'true');
region.style.position = 'absolute';
region.style.left = '-10000px';
region.style.width = '1px';
region.style.height = '1px';
region.style.overflow = 'hidden';
document.body.appendChild(region);
}
return region;
}
static announce(message, priority = 'polite') {
const region = this.createAnnouncementRegion();
region.setAttribute('aria-live', priority);
// Clear and set new message
region.textContent = '';
setTimeout(() => {
region.textContent = message;
}, 100);
// Clear after announcement
setTimeout(() => {
region.textContent = '';
}, 3000);
}
// High contrast detection
static detectHighContrast() {
if (typeof window === 'undefined') return false;
// Check for Windows high contrast mode
const testDiv = document.createElement('div');
testDiv.style.backgroundImage = 'url(data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==)';
testDiv.style.position = 'absolute';
testDiv.style.left = '-9999px';
document.body.appendChild(testDiv);
const hasBackgroundImage = window.getComputedStyle(testDiv).backgroundImage !== 'none';
document.body.removeChild(testDiv);
return !hasBackgroundImage;
}
// Color scheme preference
static getColorSchemePreference() {
if (typeof window === 'undefined') return 'light';
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
return darkModeQuery.matches ? 'dark' : 'light';
}
}
// Accessible component examples
function AccessibleModal({ visible, onClose, title, children }) {
const modalRef = React.useRef(null);
const [focusTrap, setFocusTrap] = React.useState(null);
React.useEffect(() => {
if (visible && modalRef.current) {
const trap = WebAccessibilityManager.trapFocus(modalRef.current);
setFocusTrap(trap);
// Announce modal opening
WebAccessibilityManager.announce(`${title} dialog opened`);
return () => {
trap.release();
WebAccessibilityManager.announce('Dialog closed');
};
}
}, [visible, title]);
if (!visible) return null;
return (
<View
style={styles.modalOverlay}
accessibilityRole="dialog"
accessibilityModal={true}
accessibilityLabel={title}
>
<View
ref={modalRef}
style={styles.modalContent}
accessibilityViewIsModal={true}
>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle} accessibilityRole="heading">
{title}
</Text>
<TouchableOpacity
onPress={onClose}
style={styles.closeButton}
accessibilityRole="button"
accessibilityLabel="Close dialog"
>
<Text>×</Text>
</TouchableOpacity>
</View>
<View style={styles.modalBody}>
{children}
</View>
</View>
</View>
);
}
function AccessibleForm() {
const [formData, setFormData] = React.useState({
name: '',
email: '',
message: ''
});
const [errors, setErrors] = React.useState({});
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
setErrors(newErrors);
if (Object.keys(newErrors).length > 0) {
WebAccessibilityManager.announce('Form has errors. Please review and correct.');
return false;
}
return true;
};
const handleSubmit = () => {
if (validateForm()) {
WebAccessibilityManager.announce('Form submitted successfully');
// Handle form submission
}
};
return (
<View style={styles.form}>
<Text style={styles.formTitle} accessibilityRole="heading">
Contact Form
</Text>
<View style={styles.fieldGroup}>
<Text style={styles.label}>
Name *
</Text>
<TextInput
style={[styles.input, errors.name && styles.inputError]}
value={formData.name}
onChangeText={(name) => setFormData(prev => ({ ...prev, name }))}
accessibilityLabel="Name"
accessibilityRequired={true}
accessibilityInvalid={!!errors.name}
accessibilityErrorMessage={errors.name}
/>
{errors.name && (
<Text style={styles.errorText} accessibilityRole="alert">
{errors.name}
</Text>
)}
</View>
<View style={styles.fieldGroup}>
<Text style={styles.label}>
Email *
</Text>
<TextInput
style={[styles.input, errors.email && styles.inputError]}
value={formData.email}
onChangeText={(email) => setFormData(prev => ({ ...prev, email }))}
keyboardType="email-address"
accessibilityLabel="Email address"
accessibilityRequired={true}
accessibilityInvalid={!!errors.email}
accessibilityErrorMessage={errors.email}
/>
{errors.email && (
<Text style={styles.errorText} accessibilityRole="alert">
{errors.email}
</Text>
)}
</View>
<View style={styles.fieldGroup}>
<Text style={styles.label}>
Message
</Text>
<TextInput
style={[styles.input, styles.textArea]}
value={formData.message}
onChangeText={(message) => setFormData(prev => ({ ...prev, message }))}
multiline={true}
numberOfLines={4}
accessibilityLabel="Message"
accessibilityHint="Optional message or comments"
/>
</View>
<TouchableOpacity
style={styles.submitButton}
onPress={handleSubmit}
accessibilityRole="button"
accessibilityLabel="Submit contact form"
>
<Text style={styles.submitButtonText}>Submit</Text>
</TouchableOpacity>
</View>
);
}
// Motion-aware animation wrapper
function MotionAwareAnimation({ children, animation, reducedMotionFallback }) {
const [prefersReducedMotion, setPrefersReducedMotion] = React.useState(false);
React.useEffect(() => {
const subscription = WebAccessibilityManager.addReducedMotionListener(
setPrefersReducedMotion
);
return subscription.remove;
}, []);
const animationToUse = prefersReducedMotion
? (reducedMotionFallback || { duration: 0 })
: animation;
return children(animationToUse);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#fff'
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
accessibilityRole: 'heading'
},
statusContainer: {
flexDirection: 'row',
marginBottom: 10,
alignItems: 'center'
},
label: {
fontWeight: 'bold',
marginRight: 8,
minWidth: 120
},
value: {
flex: 1
},
button: {
backgroundColor: '#007AFF',
padding: 12,
borderRadius: 8,
marginVertical: 8,
alignItems: 'center'
},
buttonText: {
color: 'white',
fontWeight: '600'
},
announcements: {
position: 'absolute',
left: -10000,
width: 1,
height: 1,
overflow: 'hidden'
},
srOnly: {
position: 'absolute',
left: -10000,
width: 1,
height: 1,
overflow: 'hidden'
},
// Modal styles
modalOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
alignItems: 'center'
},
modalContent: {
backgroundColor: 'white',
borderRadius: 12,
padding: 20,
minWidth: 300,
maxWidth: 500,
maxHeight: '80%'
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16
},
modalTitle: {
fontSize: 18,
fontWeight: 'bold'
},
closeButton: {
padding: 8,
borderRadius: 4
},
modalBody: {
flex: 1
},
// Form styles
form: {
padding: 20
},
formTitle: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 20
},
fieldGroup: {
marginBottom: 16
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 4,
padding: 12,
fontSize: 16
},
inputError: {
borderColor: '#ff0000'
},
textArea: {
height: 100,
textAlignVertical: 'top'
},
errorText: {
color: '#ff0000',
fontSize: 14,
marginTop: 4
},
submitButton: {
backgroundColor: '#28a745',
padding: 16,
borderRadius: 8,
alignItems: 'center',
marginTop: 20
},
submitButtonText: {
color: 'white',
fontSize: 16,
fontWeight: '600'
}
});React Native Web's AccessibilityInfo implementation leverages web standards and browser APIs to provide comprehensive accessibility support:
Key Features:
prefers-reduced-motion CSS media query for motion preferencesImplementation Details:
isScreenReaderEnabled() always returns true as web assumes screen reader availabilityisReduceMotionEnabled() uses (prefers-reduced-motion: reduce) media queryannounceForAccessibility() requires custom ARIA live region implementationBest Practices:
accessibilityLabelaccessibilityRole) for proper element identificationprefers-reduced-motion for animationsinterface AccessibilityInfoStatic {
isScreenReaderEnabled(): Promise<boolean>;
isReduceMotionEnabled(): Promise<boolean>;
addEventListener(
eventName: 'reduceMotionChanged' | 'screenReaderChanged',
handler: (enabled: boolean) => void
): { remove(): void };
removeEventListener(eventName: string, handler: Function): void;
setAccessibilityFocus(reactTag: number): void;
announceForAccessibility(announcement: string): void;
fetch(): Promise<boolean>; // deprecated
}
type AccessibilityEventHandler = (enabled: boolean) => void;
interface AccessibilitySubscription {
remove(): void;
}
interface FocusTrap {
release(): void;
}Install with Tessl CLI
npx tessl i tessl/npm-react-native-web