Accessibility
Make offline-first apps accessible to all users — screen readers, keyboard navigation, and assistive technology.
Accessibility
Offline-first apps must be accessible to everyone. This guide covers general web accessibility and PWA-specific considerations.
General Web Accessibility
These principles apply to all web apps, including offline-first:
Semantic HTML
Use proper HTML elements:
<!-- Bad -->
<div onclick="submit()">Submit</div>
<!-- Good -->
<button type="submit">Submit</button><!-- Bad -->
<div class="header">Title</div>
<!-- Good -->
<header>Title</header>Focus Management
Ensure interactive elements are focusable:
/* Always visible focus indicator */
:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
/* Remove default outline for mouse users */
:focus:not(:focus-visible) {
outline: none;
}Color Contrast
Meet WCAG AA standards (4.5
for normal text):/* Ensure sufficient contrast */
.text-primary {
color: #1a1a2e; /* Dark on light - passes AA */
}
.text-warning {
color: #f59e0b; /* Must have dark background */
background: #1a1a2e;
}ARIA Labels
Add context for screen readers:
<!-- Icon-only button needs label -->
<button aria-label="Close menu">
<svg>...</svg>
</button>
<!-- Form inputs need labels -->
<label for="email">Email</label>
<input id="email" type="email" />
<!-- Status updates need aria-live -->
<div aria-live="polite" aria-atomic="true">
{syncStatus}
</div>Offline-First Specific
These considerations are unique to offline/PWA apps:
Screen Readers + Service Workers
Service Workers are transparent to screen readers:
// Screen reader users browse normally
// SW caches content, but it's just "content on the page"
// No special ARIA needed for SW itself
// What DOES need attention:
function updateUIForOffline() {
// Announce to screen reader
const status = document.getElementById('status');
status.setAttribute('aria-live', 'polite');
status.textContent = 'You are now offline. Changes will sync when reconnected.';
}Offline Status Announcements
Use aria-live regions for dynamic updates:
function OfflineBanner() {
const isOnline = useNetworkStatus();
return (
<div
role="status"
aria-live="polite"
aria-atomic="true"
className={isOnline ? 'hidden' : 'visible'}
>
<span className="sr-only">Notice: </span>
You are offline. Your changes will be saved and synced when you reconnect.
</div>
);
}Important: Use aria-live="polite" (not "assertive") so users aren't interrupted.
Sync Status Indicators
Make pending sync count accessible:
function SyncIndicator() {
const { pendingCount } = useMutationQueue();
if (pendingCount === 0) return null;
return (
<div
role="status"
aria-live="polite"
aria-label={`${pendingCount} changes pending synchronization`}
>
<span aria-hidden="true">🔄</span>
<span className="sr-only">{pendingCount} changes pending</span>
</div>
);
}
// Add sr-only CSS class
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}Keyboard Navigation in Offline Mode
Ensure all functionality works with keyboard:
function OfflineTodoApp() {
// All interactions must be keyboard accessible
return (
<div>
{/* Buttons work with Enter/Space */}
<button onClick={addTodo}>Add task</button>
{/* Forms work with Tab and Enter */}
<form onSubmit={handleSubmit}>
<input />
<button type="submit">Save</button>
</form>
{/* Dialogs manage focus correctly */}
{showDialog && (
<Dialog onClose={close}>
{/* Focus trapped in dialog */}
<button onClick={close}>Close</button>
</Dialog>
)}
</div>
);
}Focus Management After Transitions
Restore focus after navigation:
function App() {
const mainRef = useRef(null);
useEffect(() => {
// On route change, move focus to main content
mainRef.current?.focus();
}, [location]);
return (
<main ref={mainRef} tabIndex={-1}>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/app" element={<App />} />
</Routes>
</Router>
</main>
);
}Reduced Motion
Respect prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
/* Disable SW progress animations */
.sw-progress-bar {
transition: none;
animation: none;
}
/* Disable cache transition animations */
.cache-updated {
transition: none;
}
}PWA Install Accessibility
Make install prompt accessible:
function InstallPrompt({ onAccept, onDismiss }) {
return (
<dialog open>
<div role="alertdialog" aria-labelledby="install-title" aria-describedby="install-desc">
<h2 id="install-title">Install App</h2>
<p id="install-desc">Install this app for offline access and better performance.</p>
<button onClick={onAccept}>Install</button>
<button onClick={onDismiss} aria-label="Dismiss">Not now</button>
</div>
</dialog>
);
}Touch Targets
Ensure adequate size for touch (44x44px minimum):
/* Buttons and interactive elements */
button,
a,
input[type="checkbox"] {
min-height: 44px;
min-width: 44px;
padding: 12px;
}Testing Accessibility
Automated Testing
# Install axe-core
npm install @axe-core/react
// In test
import { axe } from "jest-axe";
test("should have no accessibility violations", async () => {
const { container } = render(<App />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});Manual Testing Checklist
| Test | How |
|---|---|
| Screen reader | Navigate with VoiceOver (Mac) or NVDA (Windows) |
| Keyboard | Tab through all elements, use Enter/Space/Escape |
| Zoom | Test at 200% zoom |
| Reduced motion | Enable in system preferences, verify no animations |
Common Issues in Offline Apps
| Issue | Fix |
|---|---|
| Status not announced | Add aria-live regions |
| Form errors hidden | Use aria-describedby for error messages |
| Focus lost after dialog | Return focus to trigger element |
| Loading states unclear | Add aria-busy and accessible labels |
Accessibility Checklist
- All images have alt text
- Form inputs have labels
- Color contrast meets 4.5
- Focus indicators visible
- Keyboard navigation works
- Offline status announced with
aria-live - Sync status accessible to screen readers
- Focus managed after transitions
- Reduced motion respected
- Touch targets 44x44px minimum
Next Steps
-
Debugging — test accessibility
-
Performance — optimize for assistive tech
-
Testing — include a11y tests in test suite
-
Client Registration — ensure errors are accessible
-
PWA Installability — make install accessible
Swoff