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