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 withshowModal()â 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>withforattribute â clicking the label focuses the inputaria-describedbyâ links hint and error text to the inputaria-invalidâ tells screen readers the field has an errorrole="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>withshowModal()gives us: focus trapping, Escape key dismissal, backdrop (::backdrop), inert background, and proper screen reader announcements â all built in."
Color Contrast and Visual Accessibility
| Level | Normal Text | Large Text (18px+ bold, 24px+) |
|---|---|---|
| WCAG AA | 4.5:1 | 3:1 |
| WCAG AAA | 7:1 | 4.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: nonewithout 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
| Tool | What It Catches |
|---|---|
| axe-core / axe DevTools | Automated ARIA, contrast, landmark violations |
| Lighthouse Accessibility | Automated audit with scoring |
| VoiceOver (Mac) / NVDA (Win) | Real screen reader testing |
| Keyboard-only navigation | Tab order, focus traps, missing interactions |
prefers-reduced-motion | Respecting 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-labelon 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, andaria-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)