Intermediate to senior

Frontend Interview Prep

Fourteen chapters on HTML and CSS, core JavaScript, the event loop, browser rendering, React, state, performance, accessibility, security, TypeScript, testing, machine-coding components and frontend system design, with tested code.

Chapter 9 of 14Quality and security · Web Accessibility

Web Accessibility

Accessibility (a11y) means people with disabilities can perceive, understand, navigate and interact with your product. That includes people using screen readers, keyboards only, voice control, zoom, high-contrast modes, and people with motor, cognitive or temporary limitations. It is a quality bar, a legal requirement in many places, and a frequent interview topic because it exposes whether you understand the platform. This chapter gives you the principles, the patterns, and a checklist you can apply to any component.

1. The four principles (POUR)

The Web Content Accessibility Guidelines (WCAG) organise requirements under four principles:

  • Perceivable: information can be presented in ways users can perceive (text alternatives, captions, contrast, resizable text).
  • Operable: everything works with a keyboard and other input methods, with enough time, no seizure-inducing flashes, clear navigation.
  • Understandable: readable text, predictable behaviour, helpful errors.
  • Robust: works with current and future assistive technology, which means valid, semantic markup.

Conformance levels are A, AA and AAA; AA is the usual target. Check the current WCAG version (2.2 at the time of writing) for the exact criteria.

2. Start with semantic HTML

The first rule of ARIA: do not use ARIA if a native element does the job. Native elements bring role, name, state, keyboard behaviour and focus handling for free.

NeedNative element
Button<button>
Link<a href>
Text input<input> with <label>
Checkbox, radio<input type="checkbox" / "radio">
Dropdown<select>
Dialog<dialog>
Disclosure<details> and <summary>
Landmarks<header>, <nav>, <main>, <footer>, <aside>, <section aria-label>
Headings<h1> to <h6> in logical order
Lists, tables<ul>, <ol>, <table> with <th scope> and <caption>

A div with onClick is not focusable, not announced as a button, and does not respond to Enter or Space. If you must build a custom control, you owe all of that behaviour by hand.

3. Names, roles, states

Assistive technology needs three things for every interactive element:

  • Name: what it is called (from content, a <label>, aria-label or aria-labelledby).
  • Role: what kind of thing it is.
  • State and value: expanded, selected, checked, disabled, current value.

An icon-only button needs a name: <button aria-label="Close dialog"><svg aria-hidden="true">...</svg></button>.

Text alternatives: meaningful images get descriptive alt; decorative images get alt=""; complex charts get a nearby text description or data table.

4. Keyboard support

Every action available by mouse must work by keyboard.

  • Tab order follows the DOM. Do not rearrange visual order with CSS in ways that contradict it. Avoid positive tabindex; use tabindex="0" to add a custom control to the order and tabindex="-1" to allow programmatic focus only.
  • Visible focus is mandatory. Never outline: none without a replacement; prefer :focus-visible.
  • Standard keys: Enter and Space activate buttons; Enter follows links; Esc closes overlays; arrow keys move within composite widgets (tabs, menus, listboxes, radio groups); Home and End jump to first and last.
  • Skip link ("Skip to main content") as the first focusable element.
  • No keyboard traps, except intentional ones such as a modal, which must have an exit.

Roving tabindex

Composite widgets (a toolbar, tabs) expose one tab stop; arrow keys move focus within them.

// roving focus logic: which item receives focus after a key press
function nextIndex(current, key, count) {
  switch (key) {
    case "ArrowRight": case "ArrowDown": return (current + 1) % count;            // wrap around
    case "ArrowLeft": case "ArrowUp": return (current - 1 + count) % count;
    case "Home": return 0;
    case "End": return count - 1;
    default: return current;
  }
}
assert.equal(nextIndex(0, "ArrowRight", 3), 1);
assert.equal(nextIndex(2, "ArrowRight", 3), 0);
assert.equal(nextIndex(0, "ArrowLeft", 3), 2);
assert.equal(nextIndex(1, "End", 3), 2);
assert.equal(nextIndex(1, "a", 3), 1);

5. Focus management

  • Modals: on open, move focus inside (to the dialog or its first control); trap Tab within it; make the background inert (inert attribute or aria-modal with a trap); on close, return focus to the element that opened it. The native <dialog> with showModal() does much of this.
  • Route changes in single-page apps: the browser does not announce navigation, so move focus to the new page's heading or main region and update document.title.
  • Removed elements: if the focused element disappears, send focus somewhere sensible.
// the Tab-trap rule: wrap from the last focusable to the first, and vice versa
function trapTab(index, count, shift) {
  if (shift) return index === 0 ? count - 1 : index - 1;
  return index === count - 1 ? 0 : index + 1;
}
assert.equal(trapTab(2, 3, false), 0);
assert.equal(trapTab(0, 3, true), 2);
assert.equal(trapTab(1, 3, false), 2);

6. ARIA when you need it

ARIA adds semantics; it never adds behaviour. Use it for custom widgets and dynamic states.

AttributeUse
roletells assistive tech what a custom element is (tablist, dialog, alert)
aria-label / aria-labelledbygives an accessible name
aria-describedbylinks help or error text to a control
aria-expandedwhether a disclosure or menu is open
aria-controlswhich element a control operates
aria-selected, aria-checked, aria-pressed, aria-currentstate
aria-hidden="true"hides decorative content from assistive tech (never on focusable elements)
aria-live (polite / assertive)announces dynamic changes
aria-invalid, aria-requiredform state

Live regions announce updates without moving focus: search result counts, toasts, form submission status. The region must exist in the DOM before its content changes. Use role="status" (polite) for ordinary updates and role="alert" for urgent errors.

Example: accessible disclosure

<button aria-expanded="false" aria-controls="faq1" id="faq1-btn">What is your refund policy?</button>
<div id="faq1" role="region" aria-labelledby="faq1-btn" hidden>Refunds are processed within 7 days.</div>

Toggle aria-expanded and the hidden attribute together. (The native <details> achieves this without any script.)

7. Forms

  • Every input has a visible <label> linked by for and id. Placeholders are not labels: they disappear and have poor contrast.
  • Group related fields in <fieldset> with <legend> (radio groups, addresses).
  • Identify required fields in text, not colour alone, and use required and aria-required.
  • Errors: describe what went wrong and how to fix it; place the message next to the field; link with aria-describedby; set aria-invalid="true"; on a failed submit, move focus to the first error or an error summary.
  • Use the right type, autocomplete (such as email, one-time-code, street-address) and inputmode.
function errorFieldAttrs(id, error) {
  return error
    ? { "aria-invalid": "true", "aria-describedby": `${id}-error` }
    : { "aria-invalid": "false" };
}
assert.deepEqual(errorFieldAttrs("email", "Enter a valid email"), { "aria-invalid": "true", "aria-describedby": "email-error" });
assert.deepEqual(errorFieldAttrs("email", ""), { "aria-invalid": "false" });

8. Colour, contrast and visuals

  • Contrast ratio of text against its background: at least 4.5:1 for normal text and 3:1 for large text (18.66px bold or 24px regular and above) at AA. UI components and graphical objects need 3:1.
  • Do not rely on colour alone (add icons, text or patterns for errors and status).
  • Support zoom to 200% and reflow at narrow widths without horizontal scrolling; use relative units.
  • Respect prefers-reduced-motion, prefers-contrast and prefers-color-scheme.
  • Avoid content that flashes more than three times per second.
  • Provide captions and transcripts for media; do not autoplay audio.

The contrast ratio is computable from the WCAG relative-luminance formula:

function luminance(hex) {
  const [r, g, b] = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16) / 255)
    .map(c => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function contrast(fg, bg) {
  const [l1, l2] = [luminance(fg), luminance(bg)].sort((a, b) => b - a);
  return (l1 + 0.05) / (l2 + 0.05);
}
assert.ok(Math.abs(contrast("#000000", "#ffffff") - 21) < 1e-9);          // the maximum
assert.ok(contrast("#777777", "#ffffff") < 4.5);                           // a common grey fails AA for body text
assert.ok(contrast("#595959", "#ffffff") >= 4.5);                          // a slightly darker grey passes

9. Dynamic and custom widgets

For complex widgets, follow the ARIA Authoring Practices Guide patterns for roles, states and keyboard interaction: tabs, menus, comboboxes (autocomplete), listboxes, dialogs, tree views. Example: an autocomplete needs role="combobox", aria-expanded, aria-controls pointing to the listbox, aria-activedescendant for the highlighted option, arrow-key navigation, Enter to select, Esc to close, and a live announcement of the number of results. Wherever possible use a well-tested library (Radix, React Aria, Headless UI) instead of building these from scratch.

10. Testing accessibility

Automated tools (axe, Lighthouse, eslint-plugin-jsx-a11y) catch only a portion of issues. Combine:

  1. Linting and automated checks in CI.
  2. Keyboard-only pass: can you reach and operate everything, with visible focus and a sensible order?
  3. Screen reader pass (VoiceOver, NVDA, TalkBack) on key flows: is every control named, are changes announced?
  4. Zoom and reflow to 200 to 400%, and small screens.
  5. Contrast and colour-blindness checks.
  6. Real users with disabilities where possible.

In unit and integration tests, query by role and accessible name (getByRole("button", { name: "Save" })). It both tests behaviour and enforces accessible markup.

11. Common mistakes

  • div and span buttons and links.
  • Removing the focus outline.
  • Placeholder text as the only label.
  • Missing or poor alt text, or alt on decorative images.
  • Colour-only meaning and low contrast.
  • ARIA misuse: wrong roles, aria-hidden on focusable elements, redundant roles on native elements.
  • Modals that do not trap or restore focus.
  • Silent dynamic updates (no live region).
  • Positive tabindex values and order that differs from the visual layout.
  • Treating accessibility as a late audit instead of a design and build habit.

12. Practice questions

  1. What are the first rule of ARIA and the four WCAG principles?
  2. Why is <div onClick> a poor button? What must you replicate to fix it?
  3. Build an accessible modal: what must happen on open, while open and on close?
  4. How do you make an autocomplete accessible?
  5. How do you announce dynamic content to screen reader users?
  6. What contrast ratios does WCAG AA require?
  7. How would you handle focus and announcements in a single-page app route change?
  8. How do you test accessibility beyond automated tools?
Header Logo