Fossils🌐 Web PlatformHTML Accessibility Explained
ðŸĢHatchlingHTMLAccessibilityARIAInterview

HTML Accessibility Explained

Accessibility isn't a feature — it's a quality of engineering. Your answer reveals whether you build for all users or just sighted mouse users.

HTML Accessibility Explained

Interview Question: "How do you ensure a web application is accessible?"

The Senior Answer

Don't say "add ARIA labels to everything." Say:

"Accessible engineering starts with semantic HTML — the right element for the right job. Native elements carry built-in keyboard support, focus management, and screen reader announcements. ARIA is a repair tool for when HTML falls short, not a replacement. The rule is: no ARIA is better than bad ARIA."

The Foundation: Semantic HTML First

<!-- Bad: div soup with ARIA repair -->
<div role="navigation" aria-label="Main">
  <div role="list">
    <div role="listitem"><div role="link" tabindex="0">Home</div></div>
  </div>
</div>
 
<!-- Good: semantic HTML does the heavy lifting -->
<nav aria-label="Main">
  <ul>
    <li><a href="/">Home</a></li>
  </ul>
</nav>

"The semantic version gives you keyboard navigation, focus indicators, screen reader announcements, and correct link behavior — all for free. The div version requires manually reimplementing every one of those behaviors."

Focus Management

"Focus management is the keyboard user's equivalent of visual navigation. When content changes — a modal opens, a page navigates, an item is deleted — focus must move to a logical location."

function openModal() {
  const modal = document.getElementById("modal");
  modal.showModal(); // native <dialog> handles focus trapping
 
  modal.addEventListener("close", () => {
    triggerButton.focus(); // return focus to trigger
  });
}

Focus Trap Pattern

function trapFocus(container: HTMLElement) {
  const focusable = container.querySelectorAll<HTMLElement>(
    'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];
 
  container.addEventListener("keydown", (e) => {
    if (e.key !== "Tab") return;
 
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  });
}

"In practice, use the native <dialog> element with showModal() — it handles focus trapping, Escape to close, and inert background automatically. Only build a custom focus trap when <dialog> can't meet the design requirement."

Accessible Forms

<form>
  <div class="field">
    <label for="email">Email address</label>
    <input
      id="email"
      type="email"
      aria-describedby="email-hint email-error"
      aria-invalid="true"
      required
    />
    <p id="email-hint" class="hint">We'll never share your email.</p>
    <p id="email-error" class="error" role="alert">
      Please enter a valid email address.
    </p>
  </div>
</form>

Key patterns:

  • <label> with for attribute — clicking the label focuses the input
  • aria-describedby — links hint and error text to the input
  • aria-invalid — tells screen readers the field has an error
  • role="alert" on error messages — announces dynamically when they appear

Live Regions for Dynamic Content

<!-- Polite: waits for screen reader to finish current speech -->
<div aria-live="polite" aria-atomic="true">
  3 items in your cart
</div>
 
<!-- Assertive: interrupts immediately (use sparingly) -->
<div aria-live="assertive" role="alert">
  Your session expires in 2 minutes.
</div>

"Live regions are how screen readers know about dynamic updates. Without them, a toast notification or cart count change is completely invisible to non-sighted users."

Accessible Modal Dialog (Complete Example)

<dialog id="confirm-dialog" aria-labelledby="dialog-title" aria-describedby="dialog-desc">
  <h2 id="dialog-title">Delete this item?</h2>
  <p id="dialog-desc">This action cannot be undone.</p>
  <form method="dialog">
    <button value="cancel" type="submit">Cancel</button>
    <button value="confirm" type="submit" autofocus>Delete</button>
  </form>
</dialog>
 
<button id="delete-btn" aria-haspopup="dialog">Delete item</button>
const dialog = document.getElementById("confirm-dialog");
const deleteBtn = document.getElementById("delete-btn");
 
deleteBtn.addEventListener("click", () => dialog.showModal());
 
dialog.addEventListener("close", () => {
  if (dialog.returnValue === "confirm") {
    performDelete();
  }
  deleteBtn.focus();
});

"Native <dialog> with showModal() gives us: focus trapping, Escape key dismissal, backdrop (::backdrop), inert background, and proper screen reader announcements — all built in."

Color Contrast and Visual Accessibility

LevelNormal TextLarge Text (18px+ bold, 24px+)
WCAG AA4.5:13:1
WCAG AAA7:14.5:1

"Don't rely on color alone to convey meaning. Error states need icons or text, not just red borders. Interactive elements need visible focus indicators — removing outline: none without a replacement makes the app unusable for keyboard users."

/* Don't remove focus outlines — replace them */
:focus-visible {
  outline: 2px solid var(--focus-color);
  outline-offset: 2px;
}

Testing Tools

ToolWhat It Catches
axe-core / axe DevToolsAutomated ARIA, contrast, landmark violations
Lighthouse AccessibilityAutomated audit with scoring
VoiceOver (Mac) / NVDA (Win)Real screen reader testing
Keyboard-only navigationTab order, focus traps, missing interactions
prefers-reduced-motionRespecting motion preferences

"Automated tools catch ~30% of accessibility issues. The other 70% require manual testing — screen reader walkthrough, keyboard-only navigation, and cognitive review (is the flow understandable without visuals?)."

What Interviewers Look For

  • Semantic HTML first, ARIA as fallback — not the other way around
  • Focus management awareness — what happens when a modal opens, when an item is deleted
  • Real testing experience — mentioning VoiceOver or NVDA, not just "run Lighthouse"
  • Form accessibility — labels, error messages, aria-describedby
  • Dynamic content — live regions for updates

Common Mistakes

  • Adding role="button" to a <div> instead of using <button>
  • Using aria-label on elements that already have visible text
  • Forgetting to return focus after a modal closes
  • Removing focus outlines globally with outline: none
  • Using aria-live="assertive" for everything (interrupts the user constantly)
  • Not testing with an actual screen reader

Red Flags

  • "We'll add accessibility later" — it's 10x harder to retrofit
  • Not knowing the difference between aria-label, aria-labelledby, and aria-describedby
  • Unable to describe focus management for a modal
  • No mention of keyboard navigation
  • Thinking accessibility only means screen readers (it also covers motor, cognitive, and visual impairments)