Accessibility Engineering: Building Interfaces Everyone Can Use

Implement web accessibility (a11y) as an engineering practice, not an afterthought. Covers WCAG compliance, semantic HTML, keyboard navigation, screen reader testing, ARIA patterns, color contrast, and the automated testing that catches 30% of issues before manual review catches the rest.

Accessibility Engineering: Building Interfaces Everyone Can Use

Accessibility engineering ensures that user interfaces work reliably and meaningfully for people with diverse abilities—visual, auditory, motor, and cognitive. It is not a pre-launch checklist, but a continuous practice embedded in the frontend development lifecycle. This page details how to build interfaces that are usable by everyone, from keyboard-only users to those relying on screen readers, from people with low vision to those navigating with voice commands.

Planning Accessibility into the Development Workflow

Accessibility begins not in design tools, but in the developer’s terminal. It starts when a pull request includes a a11y label and a new a11y check on the CI pipeline.

The first step is to define accessibility requirements directly in the project’s package.json:

{
  "scripts": {
    "test:a11y": "npx axe-core --config .axe.json",
    "build:a11y": "npm run build && npx axe-core --config .axe.json --reporter=html --output=reports/a11y.html",
    "a11y:ci": "npm run test:a11y && npm run build:a11y"
  },
  "axe": {
    "rules": {
      "color-contrast": { "enabled": true, "options": { "threshold": 4.5 } },
      "landmark-roles": { "enabled": true },
      "aria-allowed-roles": { "enabled": true },
      "aria-required-children": { "enabled": true },
      "aria-required-parent": { "enabled": true },
      "aria-roles": { "enabled": true },
      "aria-allowed-attr": { "enabled": true },
      "aria-attr-value": { "enabled": true }
    }
  }
}

The .axe.json file configures the automated accessibility testing tool axe-core, which is now a standard in frontend tooling. The color-contrast rule checks that text and background colors meet the WCAG 2.1 AA standard with a minimum contrast ratio of 4.5:1. The landmark-roles rule ensures that all primary regions (main, navigation, aside, banner, contentinfo) are marked with appropriate ARIA roles.

When a developer runs npm run test:a11y, axe-core executes in headless Chrome and outputs a JSON report. The CI system consumes this report and fails the build if any critical rule fails.

Ensuring Keyboard Navigation is Complete

A core requirement in accessibility is that all interactive elements must be accessible via keyboard alone. This includes focus management, keyboard shortcuts, and proper focus indicators.

The most common failure is missing tabindex on dynamic elements. When a modal opens, the focus must shift to the modal’s first interactive element, and tabbing must cycle through the modal and then return to the original context.

To enforce this, developers use a simple utility function that manages focus:

function focusFirstElement(container) {
  const focusableElements = container.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  if (focusableElements.length > 0) {
    focusableElements[0].focus();
  }
}

// Usage in a modal component
function openModal(element) {
  element.style.display = 'block';
  focusFirstElement(element);
  element.setAttribute('aria-modal', 'true');
  element.setAttribute('role', 'dialog');
}

This function selects all focusable elements—buttons, links, form controls, and all tabindex-ed elements—and sets focus to the first one. The aria-modal and role="dialog" attributes signal to assistive technologies that the modal is a standalone interaction area.

The critical edge here is that tabindex values must be used correctly. A tabindex="0" element receives focus in the natural document order. A tabindex="-1" element can be focused programmatically but is not included in the tab order. A common mistake is setting tabindex="0" on a dialog’s close button but forgetting to set tabindex="-1" on a hidden overlay. When the modal opens, users tab into the modal, but the overlay is not part of the tab sequence—leading to disjointed navigation.

Structuring Content with Semantic HTML

Semantic HTML is the foundation of accessibility. It allows screen readers to interpret the page structure and enables users to navigate quickly using headings, landmarks, and lists.

A common failure is using <div> elements for navigation menus instead of <nav>. This leads to screen readers not recognizing the navigation section as a landmark.

To ensure semantic correctness, the following pattern is used across components:

<nav aria-label="Main navigation">
  <ul>
    <li><a href="/home" aria-current="page">Home</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

The aria-label attribute explicitly names the navigation landmark. The aria-current="page" attribute indicates the current page in the navigation, which screen readers announce when the user enters the nav.

For complex components like accordions, developers use <details> and <summary>:

<details class="accordion" open>
  <summary aria-expanded="true">
    <h3>How do I reset my password?</h3>
  </summary>
  <div class="accordion-content">
    <p>Follow these steps to reset your password:</p>
    <ol>
      <li>Go to the login page.</li>
      <li>Click "Forgot password".</li>
      <li>Enter your email.</li>
      <li>Check your inbox.</li>
    </ol>
  </div>
</details>

The <details> element provides built-in keyboard support: pressing Space or Enter toggles the open state. The aria-expanded attribute reflects the state visually and audibly. The summary element acts as a heading for the content, and screen readers announce it as a region.

When a component is complex—such as a data table—the following pattern ensures accessibility:

<table aria-label="User accounts list">
  <thead>
    <tr>
      <th scope="col">Name</th>
      <th scope="col">Email</th>
      <th scope="col">Role</th>
      <th scope="col">Actions</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Anna Smith</td>
      <td>[email protected]</td>
      <td>Admin</td>
      <td>
        <button aria-label="Edit Anna Smith's profile">Edit</button>
        <button aria-label="Delete Anna Smith">Delete</button>
      </td>
    </tr>
  </tbody>
</table>

The scope="col" attribute links each header cell to its column. The aria-label on the table gives a high-level description, and each action button includes an aria-label to clarify its purpose.

Managing Dynamic Content with ARIA Live Regions

When content updates dynamically—via JavaScript, polling, or real-time events—users must be notified. Screen readers rely on ARIA live regions to announce changes.

The most common mistake is using aria-live="polite" on a chat feed but forgetting to set aria-atomic and aria-relevant.

A live region must be declared with aria-live, and the developer must decide between three values:

For a real-time chat feed, the following pattern is used:

<div id="chat-feed" aria-live="polite" aria-atomic="true" aria-relevant="additions text">
  <div class="message" aria-atomic="false">
    <span class="user">Alice</span>
    <p class="text">Hi everyone, how are you?</p>
  </div>
  <div class="message" aria-atomic="false">
    <span class="user">Bob</span>
    <p class="text">I'm good, thanks!</p>
  </div>
</div>

The aria-atomic="true" on the feed ensures that the entire content is read as a single unit. The aria-relevant="additions text" tells the screen reader that new content (additions) and changes to text (text) should trigger announcements.

When a new message is appended, the JavaScript adds it to the feed and ensures that the new element has aria-live="polite":

function appendMessage(user, text) {
  const message = document.createElement('div');
  message.className = 'message';
  message.innerHTML = `<span class="user">${user}</span><p class="text">${text}</p>`;
  message.setAttribute('aria-live', 'polite');
  document.getElementById('chat-feed').appendChild(message);
}

The screen reader announces the new message when it appears, even if the user is currently reading another section.

Testing with Real Assistive Technologies

Testing in a browser is not enough. The final validation step is testing with real screen readers and keyboard-only workflows.

Developers use the following checklist to validate accessibility in production:

  1. Keyboard-only navigation: Tab through every interactive component. Ensure focus is visible and all controls are reachable.
  2. Screen reader testing: Use NVDA (free), JAWS (commercial), or VoiceOver (macOS) to navigate the app.
  3. High-contrast mode: Enable Windows high-contrast mode or macOS’s “Reduce motion” and “Increase contrast” settings.
  4. Voice control: Navigate with voice commands (e.g., Windows Voice Access, macOS Voice Control).

The most common failure is poor focus visibility. When a button is focused, the outline is too thin or the color contrast between the outline and background is insufficient. To fix this, developers use a consistent focus style in CSS:

:root {
  --focus-outline-width: 3px;
  --focus-outline-color: #007acc;
  --focus-outline-style: solid;
  --focus-outline-offset: 2px;
}

* {
  outline: none;
}

:root {
  --focus-outline-width: 4px;
  --focus-outline-color: #005a99;
}

button:focus,
input:focus,
a:focus,
[role="button"]:focus,
[role="tab"]:focus {
  outline: var(--focus-outline-width) solid var(--focus-outline-color);
  outline-offset: var(--focus-outline-offset);
  border-radius: 4px;
}

/* Ensure focus is visible on mobile touch devices */
@media (pointer: fine) {
  button:focus,
  input:focus,
  a:focus {
    outline-width: var(--focus-outline-width);
    outline-color: var(--focus-outline-color);
  }
}

This CSS ensures that focus indicators are both visually distinct and consistent across all interactive elements.

Documenting Accessibility Intent

Accessibility is not just code—it is documentation. A README.a11y.md file is maintained in the project root, describing how accessibility is handled across the codebase.

It includes:

This document becomes the single source of truth for accessibility across the frontend team.

Common Pitfalls and Silent Failures

These silent failures become apparent only when users interact with the interface over time. They are caught not in testing, but in real use.

Accessibility engineering is not a feature—it is the foundation of user experience.

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.