Browser APIs and Modern Features

1. Web Speech API Accessibility

API Feature Accessibility Consideration Implementation Best Practice WCAG Impact
Speech Recognition Alternative input method for users with motor impairments Provide keyboard/mouse alternatives; announce recognition status; handle errors gracefully Supports WCAG 2.1.1 (Keyboard), 2.5.1 (Pointer Gestures)
Speech Synthesis May conflict with screen readers; user needs control Provide play/pause/stop controls; allow speed/volume adjustment; don't auto-play WCAG 1.4.2 (Audio Control), 2.2.2 (Pause, Stop, Hide)
Voice Commands Alternative navigation for keyboard/motor limitations Document commands; provide visual feedback; support custom phrases Supports multiple input modalities (WCAG Principle 2)
Language Support Match user's language preferences Use lang attribute; support multiple languages; respect browser language WCAG 3.1.1 (Language of Page), 3.1.2 (Language of Parts)
Permissions Clear explanation needed for microphone access Explain why permission needed; graceful degradation if denied; visual indicator when active Privacy and user control (WCAG Principle 4)

Example: Accessible Speech Recognition Implementation

<div class="voice-input">
  <button id="start-recognition" aria-pressed="false">
    <span class="icon" aria-hidden="true">🎤</span>
    Start Voice Input
  </button>
  
  <div role="status" aria-live="polite" aria-atomic="true">
    <span id="recognition-status"></span>
  </div>
  
  <label for="voice-transcript">Voice Transcript</label>
  <textarea id="voice-transcript" 
            aria-describedby="voice-help"></textarea>
  
  <p id="voice-help" class="help-text">
    Click the microphone button and speak. You can also type directly.
  </p>
</div>

<script>
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;

if (!SpeechRecognition) {
  // Graceful degradation
  document.querySelector('.voice-input').insertAdjacentHTML('beforeend',
    '<div class="note">Voice input not supported in this browser.</div>'
  );
} else {
  const recognition = new SpeechRecognition();
  const button = document.getElementById('start-recognition');
  const status = document.getElementById('recognition-status');
  const transcript = document.getElementById('voice-transcript');
  
  recognition.continuous = true;
  recognition.interimResults = true;
  recognition.lang = document.documentElement.lang || 'en-US';
  
  let isListening = false;
  
  button.addEventListener('click', () => {
    if (isListening) {
      recognition.stop();
      isListening = false;
      button.setAttribute('aria-pressed', 'false');
      button.textContent = 'Start Voice Input';
      status.textContent = 'Voice input stopped';
    } else {
      recognition.start();
      isListening = true;
      button.setAttribute('aria-pressed', 'true');
      button.textContent = 'Stop Voice Input';
      status.textContent = 'Listening...';
    }
  });
  
  recognition.onresult = (event) => {
    let interimTranscript = '';
    let finalTranscript = '';
    
    for (let i = event.resultIndex; i < event.results.length; i++) {
      const result = event.results[i];
      if (result.isFinal) {
        finalTranscript += result[0].transcript + ' ';
      } else {
        interimTranscript += result[0].transcript;
      }
    }
    
    transcript.value += finalTranscript;
    // Update status with interim results (visual only, not announced)
    status.setAttribute('aria-live', 'off');
    status.textContent = interimTranscript || 'Listening...';
    status.setAttribute('aria-live', 'polite');
  };
  
  recognition.onerror = (event) => {
    status.textContent = `Error: ${event.error}. Please try again.`;
    isListening = false;
    button.setAttribute('aria-pressed', 'false');
  };
}
</script>

Example: Accessible Text-to-Speech with Controls

<div class="tts-player">
  <button id="play-tts" aria-label="Read text aloud">▶ Play</button>
  <button id="pause-tts" aria-label="Pause reading" disabled>⏸ Pause</button>
  <button id="stop-tts" aria-label="Stop reading" disabled>⏹ Stop</button>
  
  <div class="controls">
    <label for="tts-rate">Speed: <span id="rate-value">1x</span></label>
    <input type="range" 
           id="tts-rate" 
           min="0.5" 
           max="2" 
           step="0.1" 
           value="1"
           aria-valuemin="0.5"
           aria-valuemax="2"
           aria-valuenow="1"
           aria-valuetext="1x speed">
    
    <label for="tts-volume">Volume: <span id="volume-value">100%</span></label>
    <input type="range" 
           id="tts-volume" 
           min="0" 
           max="1" 
           step="0.1" 
           value="1"
           aria-valuemin="0"
           aria-valuemax="100"
           aria-valuenow="100"
           aria-valuetext="100%">
  </div>
  
  <div role="status" aria-live="polite"><span id="tts-status"></span></div>
</div>

<script>
const synth = window.speechSynthesis;
let utterance = null;
let isPaused = false;

document.getElementById('play-tts').addEventListener('click', () => {
  if (isPaused) {
    synth.resume();
    updateStatus('Reading resumed');
  } else {
    const text = document.querySelector('article').textContent;
    utterance = new SpeechSynthesisUtterance(text);
    
    utterance.rate = parseFloat(document.getElementById('tts-rate').value);
    utterance.volume = parseFloat(document.getElementById('tts-volume').value);
    utterance.lang = document.documentElement.lang || 'en-US';
    
    utterance.onstart = () => updateStatus('Reading started');
    utterance.onend = () => updateStatus('Reading finished');
    utterance.onerror = (e) => updateStatus(`Error: ${e.error}`);
    
    synth.speak(utterance);
    updateControlStates(true);
  }
});

document.getElementById('pause-tts').addEventListener('click', () => {
  synth.pause();
  isPaused = true;
  updateStatus('Reading paused');
});

document.getElementById('stop-tts').addEventListener('click', () => {
  synth.cancel();
  isPaused = false;
  updateStatus('Reading stopped');
  updateControlStates(false);
});

function updateStatus(message) {
  document.getElementById('tts-status').textContent = message;
}

function updateControlStates(isPlaying) {
  document.getElementById('pause-tts').disabled = !isPlaying;
  document.getElementById('stop-tts').disabled = !isPlaying;
}
</script>
Speech API Conflicts: Speech synthesis may interfere with screen readers - provide option to disable. Some users rely on screen reader-specific features. Always provide text alternatives. Respect user's assistive technology preferences. Test with screen readers active to detect conflicts.

2. Geolocation and Privacy

Privacy Aspect User Need Implementation WCAG Reference
Permission Request Clear explanation of why location is needed Explain before requesting; provide context; allow denial without breaking app WCAG 3.2.2 (On Input), 3.3.2 (Labels or Instructions)
Error Handling Graceful degradation when permission denied Provide manual location entry; explain errors clearly; offer alternatives WCAG 3.3.1 (Error Identification), 3.3.3 (Error Suggestion)
Status Feedback Know when location is being accessed Visual indicator; announce to screen readers; timeout handling WCAG 4.1.3 (Status Messages)
Accuracy Options Balance privacy and functionality Allow low-accuracy option; explain tradeoffs; don't require high accuracy unnecessarily Privacy considerations (WCAG Principle 4)
Persistent Storage Control over saved location data Explain what's saved; provide delete option; respect DNT header User control and privacy

Example: Accessible Geolocation with Clear Permissions

<div class="location-feature">
  <h2>Find Nearby Stores</h2>
  
  <div class="note">
    <strong>Why we need your location:</strong>
    We'll use your approximate location to show stores within 10 miles.
    You can also enter a location manually below.
  </div>
  
  <button id="use-location">Use My Location</button>
  
  <div class="alternative">
    <label for="manual-location">Or enter location manually:</label>
    <input type="text" 
           id="manual-location" 
           placeholder="City or ZIP code"
           aria-describedby="location-help">
    <p id="location-help">We'll search near this location</p>
  </div>
  
  <div role="status" aria-live="polite"><span id="location-status"></span></div>
  <div id="results" aria-live="polite" aria-atomic="true"></div>
</div>

<script>
document.getElementById('use-location').addEventListener('click', async () => {
  const status = document.getElementById('location-status');
  const results = document.getElementById('results');
  
  if (!navigator.geolocation) {
    status.textContent = 'Geolocation not supported. Please enter location manually.';
    return;
  }
  
  status.textContent = 'Requesting location...';
  
  navigator.geolocation.getCurrentPosition(
    // Success
    async (position) => {
      const { latitude, longitude, accuracy } = position.coords;
      
      status.textContent = `Location found (accurate to ${Math.round(accuracy)} meters)`;
      
      // Fetch nearby stores
      const stores = await findNearbyStores(latitude, longitude);
      results.innerHTML = `
        <h3>Found ${stores.length} stores nearby</h3>
        <ul>${stores.map(s => `<li>${s.name} - ${s.distance} miles</li>`).join('')}</ul>
      `;
    },
    // Error
    (error) => {
      let message = '';
      switch(error.code) {
        case error.PERMISSION_DENIED:
          message = 'Location access denied. Please enter location manually or enable location permissions in your browser settings.';
          break;
        case error.POSITION_UNAVAILABLE:
          message = 'Location information unavailable. Please try again or enter location manually.';
          break;
        case error.TIMEOUT:
          message = 'Location request timed out. Please try again or enter location manually.';
          break;
        default:
          message = 'Unable to get location. Please enter location manually.';
      }
      status.textContent = message;
    },
    // Options
    {
      enableHighAccuracy: false, // Better privacy
      timeout: 10000,
      maximumAge: 300000 // 5 minutes
    }
  );
});
</script>
Geolocation Best Practices:
  • Always provide manual location input as alternative
  • Explain what location will be used for before requesting
  • Don't request location on page load - wait for user action
  • Handle all error cases with clear, actionable messages
  • Consider using lower accuracy (enableHighAccuracy: false) for privacy
  • Announce status changes to screen reader users
  • Don't break functionality if permission is denied

3. File API Accessibility

File Operation Accessibility Challenge Solution Best Practice
File Input Drag-and-drop not keyboard accessible Provide standard file input; label properly; announce file selection Always include <input type="file"> with visible label
Drag and Drop Mouse-only interaction Provide keyboard alternative; announce drop zones; visual focus indicators WCAG 2.1.1 (Keyboard), supplemental to file input
File Validation Error messages need to be announced Use live regions; associate errors with input; provide clear messages WCAG 3.3.1 (Error Identification), 3.3.3 (Error Suggestion)
Upload Progress Visual-only progress bars Use <progress> or role="progressbar" with aria-valuenow; announce milestones WCAG 4.1.3 (Status Messages)
Multiple Files List management not clear Announce count; provide remove buttons; keyboard navigation for list Clear structure and controls
File Preview Images need alt text; documents need alternatives Extract filename as fallback; provide download option; describe content WCAG 1.1.1 (Non-text Content)

Example: Accessible File Upload with Drag-and-Drop

<div class="file-upload">
  <label for="file-input">
    Choose files to upload
    <span class="help-text">(or drag and drop)</span>
  </label>
  
  <div class="drop-zone" 
       role="button"
       tabindex="0"
       aria-describedby="drop-instructions">
    <input type="file" 
           id="file-input" 
           multiple 
           accept="image/*,.pdf"
           aria-describedby="file-requirements">
    <span class="drop-text">Click to browse or drag files here</span>
  </div>
  
  <p id="drop-instructions" class="sr-only">
    Activate to select files. Supports multiple file selection.
  </p>
  
  <p id="file-requirements">
    Accepted formats: Images (JPG, PNG, GIF) and PDF. Maximum 5MB per file.
  </p>
  
  <div role="status" aria-live="polite" aria-atomic="true">
    <span id="file-status"></span>
  </div>
  
  <ul id="file-list" aria-label="Selected files"></ul>
  
  <div id="upload-progress" role="progressbar" 
       aria-valuenow="0" 
       aria-valuemin="0" 
       aria-valuemax="100"
       aria-label="Upload progress"
       hidden>
    <div class="progress-bar"></div>
    <span class="progress-text">0%</span>
  </div>
</div>

<script>
const fileInput = document.getElementById('file-input');
const dropZone = document.querySelector('.drop-zone');
const fileList = document.getElementById('file-list');
const fileStatus = document.getElementById('file-status');
const selectedFiles = new Map();

// Standard file input
fileInput.addEventListener('change', (e) => {
  handleFiles(e.target.files);
});

// Drag and drop handlers
['dragenter', 'dragover'].forEach(eventName => {
  dropZone.addEventListener(eventName, (e) => {
    e.preventDefault();
    dropZone.classList.add('drag-over');
    dropZone.setAttribute('aria-dropeffect', 'copy');
  });
});

['dragleave', 'drop'].forEach(eventName => {
  dropZone.addEventListener(eventName, (e) => {
    e.preventDefault();
    dropZone.classList.remove('drag-over');
    dropZone.removeAttribute('aria-dropeffect');
  });
});

dropZone.addEventListener('drop', (e) => {
  const files = e.dataTransfer.files;
  handleFiles(files);
});

// Keyboard activation for drop zone
dropZone.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault();
    fileInput.click();
  }
});

function handleFiles(files) {
  let validCount = 0;
  let errors = [];
  
  Array.from(files).forEach(file => {
    // Validate file
    if (file.size > 5 * 1024 * 1024) {
      errors.push(`${file.name} is too large (max 5MB)`);
      return;
    }
    
    if (!file.type.match(/^image\/.*/) && file.type !== 'application/pdf') {
      errors.push(`${file.name} is not a supported format`);
      return;
    }
    
    selectedFiles.set(file.name, file);
    validCount++;
  });
  
  updateFileList();
  
  // Announce results
  let message = '';
  if (validCount > 0) {
    message = `${validCount} file${validCount !== 1 ? 's' : ''} added. `;
  }
  if (errors.length > 0) {
    message += `${errors.length} file${errors.length !== 1 ? 's' : ''} rejected: ${errors.join(', ')}`;
  }
  fileStatus.textContent = message;
}

function updateFileList() {
  fileList.innerHTML = '';
  
  selectedFiles.forEach((file, name) => {
    const li = document.createElement('li');
    li.innerHTML = `
      ${name} (${formatFileSize(file.size)})
      <button type="button" 
              aria-label="Remove ${name}"
              data-filename="${name}">Remove</button>
    `;
    
    li.querySelector('button').addEventListener('click', (e) => {
      selectedFiles.delete(e.target.dataset.filename);
      updateFileList();
      fileStatus.textContent = `${e.target.dataset.filename} removed`;
    });
    
    fileList.appendChild(li);
  });
}

function formatFileSize(bytes) {
  if (bytes < 1024) return bytes + ' B';
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
  return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
</script>

Example: Accessible Upload Progress Indicator

<div class="upload-container">
  <button id="start-upload">Upload Files</button>
  
  <div class="progress-container" hidden>
    <progress id="upload-progress" 
              max="100" 
              value="0"
              aria-label="Upload progress">
      0%
    </progress>
    <div role="status" aria-live="polite" aria-atomic="true">
      <span id="progress-status">Ready to upload</span>
    </div>
  </div>
</div>

<script>
async function uploadFiles(files) {
  const progressBar = document.getElementById('upload-progress');
  const progressStatus = document.getElementById('progress-status');
  const container = document.querySelector('.progress-container');
  
  container.hidden = false;
  
  const totalSize = Array.from(files).reduce((sum, f) => sum + f.size, 0);
  let uploadedSize = 0;
  
  for (const file of files) {
    const formData = new FormData();
    formData.append('file', file);
    
    try {
      await new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        
        xhr.upload.addEventListener('progress', (e) => {
          if (e.lengthComputable) {
            const fileProgress = (e.loaded / e.total) * 100;
            const totalProgress = ((uploadedSize + e.loaded) / totalSize) * 100;
            
            progressBar.value = totalProgress;
            
            // Announce at 25%, 50%, 75%, 100%
            const milestone = Math.floor(totalProgress / 25) * 25;
            if (milestone > 0 && totalProgress >= milestone && totalProgress < milestone + 2) {
              progressStatus.textContent = `${milestone}% complete`;
            }
          }
        });
        
        xhr.addEventListener('load', () => {
          uploadedSize += file.size;
          resolve();
        });
        
        xhr.addEventListener('error', () => {
          reject(new Error(`Failed to upload ${file.name}`));
        });
        
        xhr.open('POST', '/upload');
        xhr.send(formData);
      });
      
    } catch (error) {
      progressStatus.textContent = error.message;
      return;
    }
  }
  
  progressStatus.textContent = 'Upload complete!';
}
</script>
File API Best Practices:
  • Always provide standard file input - drag-and-drop is supplemental
  • Validate files client-side and announce errors clearly
  • Show file list with remove buttons (keyboard accessible)
  • Use native <progress> element or proper ARIA progressbar
  • Announce progress milestones (25%, 50%, 75%, 100%)
  • Don't announce every percentage change - too verbose
  • Provide clear success and error messages

4. WebRTC Accessibility Considerations

WebRTC Feature Accessibility Challenge Solution User Benefit
Video Calls Deaf/hard of hearing users need captions Integrate real-time captioning; support sign language interpreters; provide chat alternative WCAG 1.2.4 (Captions Live)
Audio Calls Blind users need audio-only option; volume control Allow audio-only mode; provide clear audio controls; support keyboard shortcuts Reduces bandwidth; supports diverse needs
Screen Sharing Shared content may not be accessible Warn sharer to check accessibility; provide alt text for shared images; describe visual content WCAG 1.1.1 (Non-text Content)
Connection Status Visual-only indicators insufficient Announce connection changes; provide text status; use ARIA live regions WCAG 4.1.3 (Status Messages)
Call Controls Mute/unmute, camera on/off need clear state Use aria-pressed; provide keyboard shortcuts; announce state changes Clear feedback for all users
Permissions Camera/microphone access needs explanation Explain before requesting; handle denials gracefully; provide status indicators Privacy and user control

Example: Accessible Video Call Controls

<div class="video-call" role="group" aria-label="Video call controls">
  <div class="video-container">
    <video id="remote-video" 
           aria-label="Remote participant video"
           autoplay></video>
    <video id="local-video" 
           aria-label="Your video preview" 
           autoplay 
           muted></video>
  </div>
  
  <div class="controls" role="toolbar" aria-label="Call controls">
    <button id="toggle-mic" 
            aria-pressed="true" 
            aria-label="Microphone on"
            title="Mute (Ctrl+D)">
      <span aria-hidden="true">🎤</span>
    </button>
    
    <button id="toggle-camera" 
            aria-pressed="true" 
            aria-label="Camera on"
            title="Turn off camera (Ctrl+E)">
      <span aria-hidden="true">📹</span>
    </button>
    
    <button id="toggle-screen" 
            aria-pressed="false"
            aria-label="Screen sharing off"
            title="Share screen (Ctrl+Shift+E)">
      <span aria-hidden="true">🖥️</span>
    </button>
    
    <button id="end-call" 
            aria-label="End call"
            class="danger">
      End Call
    </button>
  </div>
  
  <div class="status-bar">
    <div role="status" aria-live="polite" aria-atomic="true">
      <span id="connection-status">Connected</span>
    </div>
    <div role="timer" aria-live="off" aria-atomic="true">
      <span id="call-duration">00:00</span>
    </div>
  </div>
  
  <div class="chat-alternative">
    <button id="open-chat" aria-label="Open text chat">💬 Chat</button>
  </div>
</div>

<script>
let localStream = null;
let isMicOn = true;
let isCameraOn = true;
const status = document.getElementById('connection-status');

// Toggle microphone
document.getElementById('toggle-mic').addEventListener('click', function() {
  isMicOn = !isMicOn;
  
  if (localStream) {
    localStream.getAudioTracks()[0].enabled = isMicOn;
  }
  
  this.setAttribute('aria-pressed', isMicOn);
  this.setAttribute('aria-label', isMicOn ? 'Microphone on' : 'Microphone off');
  status.textContent = isMicOn ? 'Microphone unmuted' : 'Microphone muted';
});

// Toggle camera
document.getElementById('toggle-camera').addEventListener('click', function() {
  isCameraOn = !isCameraOn;
  
  if (localStream) {
    localStream.getVideoTracks()[0].enabled = isCameraOn;
  }
  
  this.setAttribute('aria-pressed', isCameraOn);
  this.setAttribute('aria-label', isCameraOn ? 'Camera on' : 'Camera off');
  status.textContent = isCameraOn ? 'Camera turned on' : 'Camera turned off';
});

// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
  if (e.ctrlKey && e.key === 'd') {
    e.preventDefault();
    document.getElementById('toggle-mic').click();
  } else if (e.ctrlKey && e.key === 'e') {
    e.preventDefault();
    document.getElementById('toggle-camera').click();
  }
});

// Initialize WebRTC
async function startCall() {
  try {
    status.textContent = 'Requesting camera and microphone access...';
    
    localStream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true
    });
    
    document.getElementById('local-video').srcObject = localStream;
    status.textContent = 'Connected';
    
  } catch (error) {
    let message = 'Unable to access camera/microphone. ';
    
    if (error.name === 'NotAllowedError') {
      message += 'Please grant permission in your browser settings.';
    } else if (error.name === 'NotFoundError') {
      message += 'No camera or microphone found.';
    } else {
      message += 'Please check your device settings.';
    }
    
    status.textContent = message;
  }
}
</script>
WebRTC Accessibility Gaps: Real-time captioning requires third-party services or browser features. Sign language interpretation needs high-quality video. Network issues affect quality - provide status updates. Always provide text chat as alternative. Test with assistive technologies. Consider bandwidth requirements for users on limited connections.

5. Service Worker and Offline Access

Offline Feature Accessibility Requirement Implementation User Experience
Offline Detection Announce online/offline status changes Use online/offline events; update UI; announce via live region WCAG 4.1.3 (Status Messages)
Cached Content Indicate when content is stale or offline Show cache timestamp; provide refresh action; explain limitations User understanding of data freshness
Sync Status Progress and completion feedback Use progressbar or status; announce sync completion; handle errors Clear feedback on data synchronization
Offline Forms Save drafts locally; indicate pending submission Auto-save to IndexedDB; show pending badge; sync when online Prevents data loss
Error Recovery Clear explanation when offline features fail Provide actionable error messages; retry mechanisms; manual sync option WCAG 3.3.3 (Error Suggestion)
Storage Limits Warn before quota exceeded Monitor storage; provide cleanup options; explain consequences Prevent unexpected failures

Example: Accessible Offline Status Notification

<div class="app-header">
  <div class="connection-indicator" 
       role="status" 
       aria-live="polite" 
       aria-atomic="true">
    <span id="connection-badge" class="badge badge--online">Online</span>
  </div>
</div>

<div id="offline-banner" 
     role="alert" 
     class="banner warning" 
     hidden>
  <strong>You're offline.</strong> 
  Changes will be saved locally and synced when you reconnect.
  <button id="dismiss-banner" aria-label="Dismiss offline notice">×</button>
</div>

<script>
const badge = document.getElementById('connection-badge');
const banner = document.getElementById('offline-banner');

function updateConnectionStatus(isOnline) {
  if (isOnline) {
    badge.textContent = 'Online';
    badge.className = 'badge badge-online';
    banner.hidden = true;
    
    // Trigger background sync if available
    if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
      navigator.serviceWorker.ready.then(reg => {
        return reg.sync.register('sync-data');
      });
    }
  } else {
    badge.textContent = 'Offline';
    badge.className = 'badge badge-offline';
    banner.hidden = false;
  }
}

// Listen for online/offline events
window.addEventListener('online', () => updateConnectionStatus(true));
window.addEventListener('offline', () => updateConnectionStatus(false));

// Initial status
updateConnectionStatus(navigator.onLine);

// Dismiss banner
document.getElementById('dismiss-banner').addEventListener('click', () => {
  banner.hidden = true;
});
</script>

Example: Accessible Background Sync with Progress

<div class="sync-status">
  <div role="status" aria-live="polite" aria-atomic="true">
    <span id="sync-message">All changes saved</span>
  </div>
  
  <button id="manual-sync" aria-describedby="sync-description">
    Sync Now
  </button>
  <p id="sync-description" class="help-text">
    Manually sync pending changes to server
  </p>
  
  <div id="sync-progress" hidden>
    <progress id="sync-bar" max="100" value="0"></progress>
    <span id="sync-percent">0%</span>
  </div>
</div>

<script>
// Service Worker registration
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(registration => {
    console.log('Service Worker registered');
    
    // Listen for sync events from SW
    navigator.serviceWorker.addEventListener('message', (event) => {
      if (event.data.type === 'SYNC_PROGRESS') {
        updateSyncProgress(event.data.progress);
      } else if (event.data.type === 'SYNC_COMPLETE') {
        updateSyncMessage('All changes synced successfully');
      } else if (event.data.type === 'SYNC_ERROR') {
        updateSyncMessage(`Sync failed: ${event.data.error}`);
      }
    });
  });
}

// Manual sync trigger
document.getElementById('manual-sync').addEventListener('click', async () => {
  if (!navigator.onLine) {
    updateSyncMessage('Cannot sync while offline');
    return;
  }
  
  const button = document.getElementById('manual-sync');
  button.disabled = true;
  updateSyncMessage('Syncing changes...');
  
  try {
    await syncPendingChanges();
    updateSyncMessage('Sync completed successfully');
  } catch (error) {
    updateSyncMessage(`Sync failed: ${error.message}. Please try again.`);
  } finally {
    button.disabled = false;
  }
});

function updateSyncMessage(message) {
  document.getElementById('sync-message').textContent = message;
}

function updateSyncProgress(percent) {
  const progressDiv = document.getElementById('sync-progress');
  const progressBar = document.getElementById('sync-bar');
  const progressText = document.getElementById('sync-percent');
  
  progressDiv.hidden = percent === 100;
  progressBar.value = percent;
  progressText.textContent = `${percent}%`;
  
  // Announce milestones
  if (percent === 25 || percent === 50 || percent === 75 || percent === 100) {
    updateSyncMessage(`Sync ${percent}% complete`);
  }
}

async function syncPendingChanges() {
  // Get pending changes from IndexedDB
  const db = await openDatabase();
  const pending = await getPendingChanges(db);
  
  const total = pending.length;
  let synced = 0;
  
  for (const change of pending) {
    await fetch('/api/sync', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(change)
    });
    
    synced++;
    updateSyncProgress(Math.round((synced / total) * 100));
  }
}
</script>
Service Worker Accessibility Best Practices:
  • Always announce online/offline status changes
  • Indicate when viewing cached/stale content
  • Provide manual sync option for user control
  • Show clear progress for background operations
  • Handle sync failures gracefully with actionable messages
  • Persist form data locally to prevent loss during offline periods
  • Don't silently fail - inform users of sync status
  • Consider users on limited/metered connections

Browser APIs and Modern Features Summary

  • Web Speech API: Provide keyboard/mouse alternatives; announce recognition status; control TTS to avoid screen reader conflicts
  • Geolocation: Explain before requesting; provide manual input alternative; handle all error cases gracefully
  • File API: Always include standard file input; validate and announce errors; show progress with milestones; keyboard-accessible file list
  • WebRTC: Support captions for video; provide audio-only mode; announce connection status; keyboard shortcuts for controls; text chat alternative
  • Service Workers: Announce online/offline status; indicate cached content; show sync progress; handle offline forms; manual sync option
  • Common Patterns: Request permissions with clear explanation; graceful degradation; status announcements; keyboard accessibility; error handling
  • Privacy: Explain data usage; respect user preferences; provide alternatives; handle denials gracefully
  • Testing: Test offline scenarios; verify announcements; keyboard-only operation; cross-browser compatibility; assistive technology testing