CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-native-web

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.

Pending
Overview
Eval results
Files

accessibility.mddocs/

Accessibility

React Native's accessibility API adapted for web with comprehensive support for screen readers, reduced motion preferences, focus management, and WCAG-compliant accessibility features.

AccessibilityInfo

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
};

isScreenReaderEnabled()

Query whether a screen reader is currently enabled (always returns true on web).

AccessibilityInfo.isScreenReaderEnabled(): Promise<boolean>

isReduceMotionEnabled()

Query whether the user prefers reduced motion based on system settings.

AccessibilityInfo.isReduceMotionEnabled(): Promise<boolean>

addEventListener()

Add event listeners for accessibility-related changes.

AccessibilityInfo.addEventListener(
  eventName: 'reduceMotionChanged' | 'screenReaderChanged',
  handler: (enabled: boolean) => void
): { remove: () => void }

setAccessibilityFocus()

Set accessibility focus to a specific element (web implementation is no-op).

AccessibilityInfo.setAccessibilityFocus(reactTag: number): void

announceForAccessibility()

Announce a message to screen readers (web implementation is no-op, use aria-live regions instead).

AccessibilityInfo.announceForAccessibility(announcement: string): void

Usage:

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'
  }
});

Web-Specific Implementation

React Native Web's AccessibilityInfo implementation leverages web standards and browser APIs to provide comprehensive accessibility support:

Key Features:

  • Media Query Integration: Uses prefers-reduced-motion CSS media query for motion preferences
  • ARIA Support: Provides utilities for ARIA live regions, roles, and properties
  • Focus Management: Implements focus trapping and programmatic focus control
  • Screen Reader Compatibility: Works with NVDA, JAWS, VoiceOver, and other assistive technologies
  • High Contrast Detection: Detects Windows high contrast mode and other accessibility preferences

Implementation Details:

  • isScreenReaderEnabled() always returns true as web assumes screen reader availability
  • isReduceMotionEnabled() uses (prefers-reduced-motion: reduce) media query
  • announceForAccessibility() requires custom ARIA live region implementation
  • Events are based on CSS media query change listeners
  • Focus management uses native DOM APIs

Best Practices:

  • Always provide alternative text for images using accessibilityLabel
  • Use semantic roles (accessibilityRole) for proper element identification
  • Implement focus management for modals and dynamic content
  • Respect prefers-reduced-motion for animations
  • Provide error messages and validation feedback
  • Use sufficient color contrast ratios (minimum 4.5:1)
  • Ensure keyboard navigation works throughout the application

Types

interface 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

docs

accessibility.md

animation.md

core-utilities.md

form-controls.md

hooks.md

index.md

interactive-components.md

layout-components.md

list-components.md

media-components.md

platform-apis.md

stylesheet.md

system-integration.md

text-input.md

tile.json