JavaScript by Example

Accessibility Basics

Making pages work for keyboard users and screen readers - semantic HTML first, focus management for dynamic changes, and ARIA as the patch layer when native elements fall short.

Accessibility (abbreviated "a11y") means a feature works for users who are blind, can't use a mouse, or navigate with a keyboard or screen reader. The web platform handles most of this automatically when you use semantic HTML and leave keyboard behavior intact. This lesson runs in a browser; the earlier lessons run anywhere JavaScript runs.

Semantic elements like <button>, <a>, and <input> are focusable by default, respond to keyboard events (Enter, Space), and are announced correctly by screen readers. A clickable <div> has none of that - you would have to re-implement all of it yourself, imperfectly.

<!-- Semantic: works with keyboard and screen reader for free -->
<button type="button" onclick="deleteItem()">Delete</button>
 
<!-- Non-semantic: broken for keyboard users out of the box -->
<div onclick="deleteItem()">Delete</div>
// A keyboard user presses Tab to reach the button, then Enter or Space to activate it.
// The <div> version is never reachable via Tab and Enter does nothing.
// You would need tabindex="0", a keydown listener, and a role="button" to match
// what <button> gives you for free.
document.querySelector("div").addEventListener("keydown", (event) => {
  if (event.key === "Enter" || event.key === " ") {
    deleteItem(); // manually re-implementing what <button> already does
  }
});

When a DOM node is removed - say, deleting a list item - the keyboard focus disappears and jumps to <body>. The user loses their place in the page. Moving focus to the next logical target after the deletion keeps the experience coherent.

function deleteItem(itemEl) {
  const list = itemEl.parentElement;
  const next = itemEl.nextElementSibling || itemEl.previousElementSibling;
 
  itemEl.remove();
 
  if (next) {
    // Move focus to the adjacent item so the user doesn't lose their place
    next.focus();
  } else {
    // List is empty - move focus to the list's container or a nearby heading
    list.closest("section")?.querySelector("h2")?.focus();
  }
}
<!-- Items need tabindex="-1" to receive programmatic focus even if they aren't
     naturally focusable elements like <button> or <a> -->
<ul>
  <li tabindex="-1">
    Task one <button onclick="deleteItem(this.parentElement)">Delete</button>
  </li>
  <li tabindex="-1">
    Task two <button onclick="deleteItem(this.parentElement)">Delete</button>
  </li>
</ul>

Icon-only controls have no visible text for screen readers to announce. aria-label gives the control a name. aria-describedby points to a separate element that provides a longer description - useful for fields that need hint text or error messages.

<!-- aria-label names a control when there is no visible text -->
<button type="button" aria-label="Close dialog">
  <svg aria-hidden="true" focusable="false"><!-- X icon SVG --></svg>
</button>
 
<!-- aria-describedby links an input to its help text -->
<label for="username">Username</label>
<input
  id="username"
  type="text"
  aria-describedby="username-hint"
/>
<p id="username-hint">3-20 characters; letters, numbers, and underscores only.</p>
// Adding aria-describedby dynamically when a validation error appears
const input = document.querySelector("#username");
const errorEl = document.querySelector("#username-error");
 
function showError(message) {
  errorEl.textContent = message;
  input.setAttribute("aria-describedby", "username-error");
  input.setAttribute("aria-invalid", "true");
}
 
function clearError() {
  errorEl.textContent = "";
  input.removeAttribute("aria-describedby");
  input.removeAttribute("aria-invalid");
}

In production

If a feature only works with a mouse, it ships broken - keyboard support is not optional, it is a legal requirement in many jurisdictions (WCAG 2.1 AA). Visible focus rings and semantic HTML solve 90% of accessibility problems; ARIA is the patch layer for cases where native semantics don't reach (custom dropdowns, date pickers). Reach for ARIA only after native elements fail you, because incorrect ARIA is worse than no ARIA.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free