Building Accessible Web Components (a11y Guide)
A comprehensive developer guide to ARIA roles, keyboard navigation, focus management, and screen reader testing.
Listen to Article
PlayingClick play to listen to audio narration

Table of Contents
Web accessibility (a11y) ensures that all users, regardless of disability or device, can navigate and interact with your web content.
Core Pillars of Web Accessibility
- Keyboard Navigation: Ensure every interactive element can be reached and activated using
TabandEnter/Space. - Semantic HTML: Prefer
<button>,<nav>,<header>,<main>over unsemantic<div>tags with click handlers. - ARIA Roles & States: Use
aria-expanded,aria-hidden, andaria-liveappropriately when building custom components.
Accessible Modal Dialog Pattern
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
class="modal"
>
<h2 id="modal-title">Settings</h2>
<button aria-label="Close dialog">X</button>
</div>
Testing Accessibility
Use automated testing tools like axe-core, Lighthouse Accessibility audits, and manual screen reader testing (NVDA, VoiceOver) to catch contrast and keyboard traps early.
Focus Management in Modal Dialogs
When a modal opens, focus must move into the dialog. When it closes, focus must return to the element that triggered it. Without this, keyboard users lose their place.
function openModal(modal, trigger) {
modal.removeAttribute('hidden');
modal.setAttribute('aria-hidden', 'false');
// Move focus to first focusable element
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusable.length) focusable[0].focus();
// Trap focus inside modal
modal.addEventListener('keydown', trapFocus);
modal._trigger = trigger;
}
function closeModal(modal) {
modal.setAttribute('hidden', '');
modal.setAttribute('aria-hidden', 'true');
modal.removeEventListener('keydown', trapFocus);
if (modal._trigger) modal._trigger.focus();
}
function trapFocus(e) {
if (e.key !== 'Tab') return;
const focusable = [...this.querySelectorAll(
'button:not([disabled]), [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)].filter(el => !el.closest('[hidden]'));
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault(); last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault(); first.focus();
}
}
Colour Contrast Requirements
WCAG 2.1 level AA requires:
- Normal text (below 18pt / 14pt bold): 4.5:1 contrast ratio
- Large text (18pt+ / 14pt+ bold): 3:1 contrast ratio
- UI components and graphical objects: 3:1
Use the WebAIM Contrast Checker or the browser DevTools accessibility panel to verify your palette.
Live Regions for Dynamic Content
When content updates without a page load (search results, notifications, status messages), announce them to screen readers using aria-live:
<!-- Polite: waits for the user to finish current activity -->
<div aria-live="polite" aria-atomic="true" class="sr-only" id="status"></div>
<!-- Assertive: interrupts immediately (use sparingly) -->
<div aria-live="assertive" aria-atomic="true" class="sr-only" id="alert"></div>
// Announce a status update
document.getElementById('status').textContent = '12 results found';
Common Accessibility Mistakes
| Mistake | Impact | Fix |
|---|---|---|
onclick on a <div> | Not keyboard accessible | Use <button> instead |
Image with empty alt that conveys content | Screen reader skips it | Write a descriptive alt |
| Placeholder text as the only label | Disappears on focus | Add a visible <label> |
tabindex="2" (positive) | Breaks tab order | Use tabindex="0" only |
| Icon button with no label | Meaningless to screen reader | Add aria-label |
Automated Testing Tools
- axe-core (browser extension + JS library): catches ~57% of WCAG issues automatically.
- Lighthouse Accessibility Audit: built into Chrome DevTools under the Lighthouse tab.
- Pa11y: command-line tool for CI accessibility testing.
# Run Pa11y against a local URL
npx pa11y http://localhost:4321/blog/my-article
Manual testing with a screen reader remains essential. VoiceOver (macOS/iOS) and NVDA (Windows) together cover the majority of screen reader users.
Written by Editorial Team
Tech contributor covering software architecture, AI research, cloud infrastructure, and systems engineering practices.