General5 min read

Building Accessible Web Components (a11y Guide)

A comprehensive developer guide to ARIA roles, keyboard navigation, focus management, and screen reader testing.

E

Listen to Article

Click play to listen to audio narration

Building Accessible Web Components (a11y Guide)

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

  1. Keyboard Navigation: Ensure every interactive element can be reached and activated using Tab and Enter/Space.
  2. Semantic HTML: Prefer <button>, <nav>, <header>, <main> over unsemantic <div> tags with click handlers.
  3. ARIA Roles & States: Use aria-expanded, aria-hidden, and aria-live appropriately 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

MistakeImpactFix
onclick on a <div>Not keyboard accessibleUse <button> instead
Image with empty alt that conveys contentScreen reader skips itWrite a descriptive alt
Placeholder text as the only labelDisappears on focusAdd a visible <label>
tabindex="2" (positive)Breaks tab orderUse tabindex="0" only
Icon button with no labelMeaningless to screen readerAdd 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.

E

Written by Editorial Team

Tech contributor covering software architecture, AI research, cloud infrastructure, and systems engineering practices.

View Profile
Recommended For You

Related Articles

Quick:
Navigate Select
Loading search index...