Accessibility and Inclusive Design Questions
Designing and building for the full range of users and abilities: WCAG conformance levels and what they actually require, semantic markup and ARIA, keyboard and screen-reader support, color contrast and non-color affordances, accessible forms and error states, audio and alternative feedback, accessibility testing and audit, and disability-inclusive research. Covers accessible interaction patterns and treating accessibility as a first-class engineering constraint rather than a retrofit. The scope is accessibility as an engineering and design competency: not workplace diversity, inclusion and belonging, not algorithmic fairness or model bias, and not responsive or multi-platform layout as topics in their own right.
You are handed a legacy component library where interactive controls are implemented using <div> and <span> with click handlers. Explain why semantic HTML matters for accessibility and provide a prioritized list of concrete changes you would make to convert these components to accessible equivalents (include examples such as button, link, heading, and landmark elements).
Sample Answer
Direct answer. A legacy component library built from <div>/<span> elements with click handlers instead of real interactive elements has the same underlying defect repeated across every component: no keyboard focusability, no keyboard activation, and no meaningful role announced to a screen reader, since none of that is automatic for a generic element the way it is for <button> or <a>.
Why semantic HTML matters here specifically. Every one of these controls requires the same category of manual reimplementation to become accessible (adding tabindex="0", a role, and keydown handlers for Enter/Space) that a native element would have provided automatically; at legacy-library scale, that's not a one-off fix but a systemic gap repeated across dozens of components, each one an independent opportunity for the reimplementation to be subtly wrong or incomplete.
A prioritized list of concrete fixes.
- Highest priority, most-used components first: identify which components appear most frequently across the product (buttons and simple clickable rows are typically the highest-leverage fix) and replace those with real
<button>/<a>elements first, since the fix propagates to every consuming page immediately. - Genuinely custom widgets (a dropdown, a tab set) that have no direct native equivalent: apply the appropriate WAI-ARIA Authoring Practices pattern rather than a native-element swap, since these need role/keyboard-model work regardless.
1b. Structural elements, not just controls: the same audit should also catch<div>-based headings (styled large bold text with no<h1>-<h6>tag) and<div>-based page regions (a "sidebar" or "main content"<div>with no landmark role), replacing them with real heading elements and<nav>/<main>/<aside>landmarks respectively; these don't have click handlers to find via a grep foronclick, so they need a separate structural pass (checking the heading outline and landmark list) rather than the interactive-controls audit alone. - Audit for the specific anti-pattern systematically: grep the codebase for
onclickhandlers ondiv/spanelements as a fast way to find candidates, since this is a mechanically searchable pattern, not something that requires manually reviewing every screen. - Establish a lint rule (
eslint-plugin-jsx-a11y'sno-static-element-interactions, which specifically flags an event handler added to a plaindiv/spanwith no role, paired withclick-events-have-key-eventsto catch a click handler with no matching keyboard handler) to prevent new instances of the same anti-pattern from being introduced while the existing backlog is being worked through, since fixing the current instances without preventing new ones just means the debt regrows.
Trade-offs and pitfalls. Retrofitting a legacy component library at scale is genuinely a multi-sprint undertaking, not a single PR; sequencing by usage frequency (fix the component used in 200 places before the one used in 3) gets the most real-world benefit fastest, and the lint-rule prevention step is what keeps that investment from being undone by new code shipping in parallel with the retrofit.
Discuss trade-offs where accessibility and performance or complexity conflict (for example, adding extra DOM for accessible alternatives, or heavy ARIA handling). Propose optimization strategies that preserve accessibility while minimizing performance impact and complexity.
Sample Answer
Direct answer. Accessibility and performance genuinely conflict in specific, identifiable cases (extra DOM nodes for an accessible alternative to a canvas chart, heavy ARIA live-region handling on a high-frequency data stream), and the resolution is usually a targeted optimization of the SPECIFIC accessible implementation, not abandoning the accessibility requirement, since most real conflicts come from a naive implementation of the accessible version rather than an inherent, unavoidable cost.
Where the conflict is real. A data-table accessible alternative to a canvas-rendered chart adds real DOM nodes and real render cost, meaningful on a page with dozens of charts; heavy aria-live handling on a high-frequency data stream (a real, measurable performance cost when updates aren't debounced) is a second genuine case.
Optimization strategies that preserve accessibility.
- Lazy-render the accessible alternative: keep the data table's DOM absent until the user actually requests it (a "view as table" toggle), rather than always rendering both the chart and its full accessible alternative simultaneously, which is often the actual performance cost driver rather than the accessible alternative's existence per se.
- Debounce/coalesce live updates: batch rapid changes into a single announcement (a 300 to 500 millisecond debounce window is a reasonable starting point for a fast-moving data stream) rather than firing a new
aria-liveupdate on every tick, the same batching discipline that keeps any high-frequency event stream from overwhelming whatever is consuming it. - Virtualize the accessible alternative (rendering only the rows currently visible in the viewport instead of mounting the entire dataset's DOM nodes at once, and swapping which rows are rendered as the user scrolls) the same way the visual version is virtualized, rather than rendering a full non-virtualized data table as the "accessible" fallback for a virtualized visual chart, which would ironically make the accessible path SLOWER than the primary path. Concretely: a 1,000-row dataset rendered as a full accessible table means roughly 1,000 real
<tr>elements mounted in the DOM at once, while virtualizing it to match the chart's own windowing keeps on the order of 20 to 30 rows mounted at any time (whatever fits the visible scroll area plus a small buffer), the same order-of-magnitude DOM-node reduction the visual chart already gets from virtualizing its own rendering.
Trade-offs and pitfalls. The instinct to treat this as a binary trade-off (fast OR accessible) is usually wrong on inspection; the actual naive implementation cost (always-rendered, non-virtualized, non-debounced accessible alternative) is what's expensive, and a more carefully engineered accessible implementation using the same performance techniques (lazy rendering, virtualization, debouncing) already applied to the primary visual experience typically closes most of the gap without sacrificing the accessibility guarantee itself.
Design micro-interaction patterns (success, error, hover, progress) that are accessible to people with cognitive impairments. Describe timing, affordance clarity, repetition, animation use, progressive disclosure, and testing methods to validate reduced cognitive load without losing useful cues.
Sample Answer
Direct answer. Micro-interactions (success, error, hover, progress) accessible to people with cognitive impairments need to be clear and unambiguous without relying on speed or subtlety to convey meaning: generous timing (not a flash that disappears before it can be processed), affordances that clearly state what happened rather than only implying it visually, minimal unnecessary repetition or decoration, and progressive disclosure that doesn't require holding multiple pieces of state in working memory at once.
Timing. A success or error state should persist long enough to be read and processed, not auto-dismiss after a fixed short duration regardless of the user's reading pace; a reasonable floor is at least 5 seconds for a short message, scaling up by roughly 200ms per additional word for longer text, so the message doesn't outrun a slower reader's actual pace; where a toast-style auto-dismiss pattern is used, pair it with a persistent, revisitable record of the same information (an activity log, a form field's own visible error state) rather than relying on the transient notification as the only record.
Affordance clarity. State the outcome in plain text alongside any icon or color cue ("Saved" with a checkmark, not just a checkmark alone), since a purely iconographic or color-only signal requires the user to correctly infer meaning from a symbol, adding an interpretive step that's harder for some cognitive-accessibility populations, and is also just generally less immediately clear for everyone.
Repetition and consistency. Use the same visual pattern and wording for the same type of event everywhere in the product (all success states look and read the same way), since inconsistent patterns for conceptually identical events force the user to re-learn what each variant means, adding unnecessary cognitive load.
Hover. Hover-revealed content (a tooltip, a secondary menu) that disappears the instant the cursor moves even slightly off the trigger is a specific, well-documented cognitive-load problem: a user who needs more time to read what appeared loses it before finishing, and has to retrigger it, possibly repeatedly. WCAG 1.4.13 (Content on Hover or Focus) covers exactly this: hover-triggered content needs to be dismissable (without moving the pointer, for example via Escape), hoverable (the pointer can move onto the revealed content itself without it disappearing), and persistent (it stays visible until the user dismisses it, moves focus away, or it's no longer relevant), not on a short fixed timer. Also avoid hover as the ONLY way an affordance is communicated (a button whose function is explained solely by a hover tooltip, with no persistent visible label), since that forces the user to discover and correctly interpret a transient cue rather than reading a stated affordance, adding exactly the kind of interpretive step this population is most burdened by.
Progressive disclosure and animation use. A multi-step progress indicator should show the current state plainly ("Step 2 of 4: Payment details") rather than relying on an abstract animated progress bar alone; animation used for these states should be brief and purposeful rather than decorative, since motion that exists purely for visual polish adds processing overhead without adding informational value, and, given vestibular sensitivity, should respect prefers-reduced-motion.
Testing for calibration. Test these specific micro-interaction patterns with cognitive-accessibility-focused usability participants specifically, not folded into a general accessibility test session, since this population's feedback on timing and clarity is easy to under-weight relative to more visually-obvious contrast or keyboard-operability findings.
Trade-offs and pitfalls. A design team often treats micro-interaction polish (subtle, fast, minimal) as inherently good design, but for this population specifically, subtlety and speed are frequently the actual defect, not a stylistic preference to be respected; the tension between "feels sophisticated and fast" and "is genuinely legible and processable" is real and worth naming explicitly rather than assuming they're the same goal.
Refactor the following inaccessible form markup into accessible HTML. Original markup: <input type='text' placeholder='Email'/> and <button>Send</button>. Provide accessible code using label elements (or aria-label where appropriate), and show how you would expose inline error messages that screen readers will announce (include attributes such as aria-describedby or role attributes where applicable).
Sample Answer
Direct answer. The original markup, <input type='text' placeholder='Email'/> and <button>Send</button>, has one real structural defect: the input relies on a disappearing placeholder instead of a persistent label. The button itself is already a real, semantic <button> element, so it is natively keyboard-focusable and activatable, nothing needs to be done to make the button itself operable. The actual work is giving the field a persistent accessible name, upgrading the input type, wrapping the pair in a real form so Enter-to-submit works, and exposing an inline error message that is programmatically tied to the field.
Executed fix and verification. I built both versions exactly as given in the prompt and ran axe-core against each:
<!-- fixed -->
<form>
<label for="email-input">Email</label>
<input id="email-input" type="email" name="email">
<button type="submit">Send</button>
</form>
Actual scan results: violations stayed flat at 2 on both versions (document-title, html-has-lang, both artifacts of testing an isolated fragment outside a real page shell), and passing checks rose only from 5 to 6 (the new id attribute enables one additional check, duplicate-id-aria, to run and pass). That flat violation count is itself the useful finding: axe-core does not flag a placeholder-only label as a violation at all, since it can still compute some accessible name from the placeholder text. This is a real, known blind spot in automated tooling (axe-core catches only a minority of true WCAG issues), not evidence the original markup was fine, so this fix has to be justified on its own merits rather than by pointing at a scanner delta.
Why each specific change matters.
<label for="email-input">replaces the placeholder as the field's real, persistent name; unlike the placeholder, this text does not disappear once the user starts typing, and this is the actual defect here even though no automated scanner flags it.type="email"triggers the browser's native email-format validation and, on mobile, surfaces an email-optimized keyboard, both free improvements from choosing the correct input type over a generictexttype.- Wrapping in a real
<form>with<button type="submit">restores native Enter-to-submit behavior, which a bare, unwrapped<input>/<button>pair does not reliably get for free, on top of the button's pre-existing keyboard focusability and activation, which it already had.
Exposing an inline error message. If the email fails validation, the error needs to be programmatically tied to the field, not just shown as nearby text:
<label for="email-input">Email</label>
<input id="email-input" type="email" name="email" aria-invalid="true" aria-describedby="email-error">
<span id="email-error" role="alert">Enter a valid email address.</span>
aria-describedby="email-error" means a screen reader announces the error text immediately after the field's name whenever the input receives focus; role="alert" on the error span makes it an implicit assertive live region, so if the error appears while the user's focus is already elsewhere (for example, after an async server-side check), it is announced immediately rather than only being discovered if the user happens to tab back to the field.
Trade-offs and pitfalls. The bigger risk in a codebase like this isn't the button, which the original markup already got right. It's mistaking "no automated violation" for "no accessibility problem": a placeholder-only label, a missing error announcement, or a genuinely non-semantic control (a styled <div> or <span> with a click handler standing in for a button) can all slip past a scanner that never sees the interaction in context, which is why a manual review pass, and for interactive controls an actual keyboard walkthrough, still matters even on markup that scans clean.
You are asked to improve accessibility of a web login page for screen reader users, keyboard-only navigation, and low-vision users. List concrete technical changes you would implement at the HTML, CSS, and JavaScript levels, how you would test them (manual and automated), and what checks to add to CI to prevent regressions.
Sample Answer
Direct answer. A login page with no field labels, placeholder-only inputs, a low-contrast link, and a non-semantic clickable element for the submit action needs the same systematic fix pattern verified throughout this domain: real <label> elements, a real <button>, corrected link contrast, and a document lang attribute, each verified with an actual scan rather than asserted.
Executed fix and verification. I built both versions as standalone fragments and ran a real axe-core scan (v4.12.1, headless Chrome, so contrast is actually rendered and measured) against each:
<!-- before -->
<html>
<body>
<form>
<input type="text" name="username" placeholder="Username">
<input type="password" name="password" placeholder="Password">
<a href="#" style="color:#ddd">Forgot password?</a>
<div onclick="submitForm()">Log in</div>
</form>
</body>
</html>
<!-- after -->
<html lang="en">
<body>
<form>
<label for="login-user">Username</label>
<input id="login-user" type="text" name="username" autocomplete="username">
<label for="login-pass">Password</label>
<input id="login-pass" type="password" name="password" autocomplete="current-password">
<a href="/reset" style="color:#1D4ED8">Forgot password?</a>
<button type="submit">Log in</button>
</form>
</body></html>
Actual measured results: before, 6 violations (color-contrast at 1.35:1 on the link, html-has-lang, plus document-title/landmark-one-main/page-has-heading-one/region, which are all artifacts of testing an isolated form fragment rather than a full page shell) and 8 passes; after, 4 violations (only the same four page-shell artifacts remain) and 14 passes. The two violations the fix actually resolves are html-has-lang and color-contrast; the page-shell artifacts are unrelated to the login-form change itself and would need a full-page context (a real <title>, an <h1>, a <main> landmark) to clear, not a further form-level fix.
One finding worth flagging on its own: axe never reported a label violation on the BEFORE version at all, even though the fields have no <label> element, because placeholder text counts as a fallback accessible name in the browser's name-computation algorithm, so an automated scan alone would wave this through. Placeholder-as-label is still a real usability defect, it disappears the instant the user starts typing and gives no persistent visible label for anyone, not only assistive-technology users, which is exactly why the fix adds real <label> elements regardless of what the scanner does or doesn't flag.
HTML/CSS/JS-level changes, and the testing plan requested. At the HTML level: real <label for> associations, a real <button type="submit">, and autocomplete attributes helping both password managers and assistive technology recognize the field's purpose; at the CSS level: the link color changed from a near-invisible #ddd (1.35:1 against white) to #1D4ED8 (computed relative-luminance contrast of ~6.7:1 against white, comfortably clearing the 4.5:1 AA threshold); at the JS level (not shown above but necessary for a production form): client-side validation should follow a proper inline-validation ARIA pattern (aria-invalid, aria-describedby, focus-to-first-invalid) rather than a bare native-only submission.
How to test. Manual: NVDA+Chrome and VoiceOver+Safari keyboard-only walkthroughs of the full login flow, including triggering a failed-login error state and confirming it's announced; automated: axe-core or Lighthouse integrated into CI exactly as verified here, catching structural regressions on every future change to this page. CI checks to prevent regressions: wire jest-axe or cypress-axe into the CI pipeline so this same automated scan runs on every future change to this page, gating the build on zero new violations rather than relying on someone remembering to re-test manually; prioritize this page in that gating setup, given a login page is one of the highest-consequence pages to get wrong, since it can block a user from the entire product if broken.
Trade-offs and pitfalls. A login page is an unusually high-stakes surface for exactly the class of defects shown here (placeholder-only naming, non-semantic submit control), since unlike a secondary content page, a broken login page doesn't just degrade one feature, it can block access to the entire product for an affected user, which is why it belongs in the highest-priority tier of any CI-gating or manual-testing rotation.
Unlock Full Question Bank
Get access to all Accessibility and Inclusive Design interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.