Accessibility in Firefox Extensions: ARIA, Focus Management, and Screen Readers
Browser extensions should work for everyone. Most accessibility mistakes are easy to fix once you know what to look for.
The Basics: Semantic HTML First
The most important accessibility tool is HTML semantics. Use the right element for the job:
<!-- BAD: div soup with ARIA bolted on -->
<div role="button" tabindex="0" onclick="search()">Search</div>
<!-- GOOD: native button handles keyboard, focus, and ARIA automatically -->
<button type="submit">Search</button>
<!-- BAD: fake heading -->
<div class="title">Weather & Clock Dashboard</div>
<!-- GOOD: real heading -->
<h1>Weather & Clock Dashboard</h1>
Native elements are always better than ARIA-enhanced divs. Use ARIA only when native elements don't cover your use case.
Focus Management
For new tab pages, autofocus is appropriate:
<input type="search" autofocus placeholder="Search..." />
But don't steal focus from everything — only autofocus when it makes sense contextually.
For modal dialogs or settings panels:
function openSettings() {
const modal = document.getElementById('settings-modal');
modal.removeAttribute('hidden');
modal.setAttribute('aria-hidden', 'false');
// Focus the first focusable element
const firstFocusable = modal.querySelector('button, input, select, [tabindex]:not([tabindex="-1"])');
firstFocusable?.focus();
}
function closeSettings() {
const modal = document.getElementById('settings-modal');
modal.setAttribute('hidden', '');
modal.setAttribute('aria-hidden', 'true');
// Return focus to the button that opened it
document.getElementById('settings-btn').focus();
}
Focus Trapping in Modals
function trapFocus(modal) {
const focusable = modal.querySelectorAll(
'button, input, select, textarea, a[href], [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
modal.addEventListener('keydown', e => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
}
ARIA Live Regions for Dynamic Content
For content that updates (weather, clocks), announce important changes:
<!-- Polite: waits for user to finish current action -->
<div aria-live="polite" aria-atomic="true" id="weather-announcement" class="sr-only"></div>
<!-- Assertive: interrupts (use sparingly) -->
<div aria-live="assertive" id="error-announcement" class="sr-only"></div>
function announceWeatherUpdate(description) {
const el = document.getElementById('weather-announcement');
el.textContent = ''; // Clear first to re-trigger for same text
requestAnimationFrame(() => {
el.textContent = `Weather updated: ${description}`;
});
}
For clocks — don't announce every second. That would be maddening. Only announce on explicit user action.
Screen Reader Only Text
.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;
}
Use for context that sighted users get visually but screen reader users need explicitly:
<!-- Temperature display: sighted users see "15°C" -->
<!-- Screen readers need context: "Current temperature: 15 degrees Celsius" -->
<div aria-label="Current temperature: 15 degrees Celsius">
15°C
</div>
Color Contrast
WCAG 2.1 requires:
- Normal text: 4.5:1 contrast ratio
- Large text (18px+ or 14px bold+): 3:1
Check your theme in both light and dark mode. Tools:
- WebAIM Contrast Checker
- Firefox DevTools → Accessibility → Check for issues
/* Example: ensure sufficient contrast */
:root {
--bg: #ffffff;
--text: #1a1a1a; /* 17:1 contrast on white — excellent */
--text-muted: #767676; /* 4.54:1 on white — just passes AA */
}
Keyboard Navigation for Custom Widgets
If you build a custom radio group, tab list, or carousel:
// Theme toggle keyboard navigation
const themeButtons = document.querySelectorAll('.theme-toggle [data-theme]');
themeButtons.forEach((btn, i) => {
btn.addEventListener('keydown', e => {
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
const next = themeButtons[(i + 1) % themeButtons.length];
next.focus();
next.click();
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
const prev = themeButtons[(i - 1 + themeButtons.length) % themeButtons.length];
prev.focus();
prev.click();
}
});
});
Testing
- Keyboard only: Unplug your mouse. Can you use the extension?
- Firefox Accessibility Inspector: F12 → Accessibility tab → check for issues
- Screen reader: NVDA (Windows) or Orca (Linux) — free and widely used
- Zoom to 200%: Does layout break?
Weather & Clock Dashboard — free Firefox new tab extension. Built with accessibility in mind: keyboard nav, ARIA labels, semantic HTML. MIT licensed.
Top comments (0)