Fossils⚛ïļ React PatternsAccessibility Interview Questions
ðŸĢHatchlingReactAccessibilityARIAKeyboard Navigationa11y

Accessibility Interview Questions

ARIA roles, accessible autocomplete, keyboard navigation — the a11y questions that show you build for everyone.

Accessibility Interview Questions

Accessibility (a11y) questions have gone from "nice to mention" to "you must know this." Companies face legal requirements (ADA, WCAG), and interviewers use a11y questions to test whether you build production-grade UIs or just tutorial-level ones. If you can talk about ARIA, keyboard navigation, and screen readers, you stand out from 90% of candidates.


Q29: What Is ARIA?

Interview Question: "What is ARIA? When should you use it — and when should you NOT?"

Think of it like subtitles on a movie. If the movie is well-acted with clear dialogue (semantic HTML), most people don't need subtitles. But for people who can't hear the audio (screen reader users who can't see the visual UI), subtitles make the movie accessible. ARIA is those subtitles — it adds invisible labels and descriptions that assistive technologies can read.

ARIA stands for Accessible Rich Internet Applications. It's a set of HTML attributes that provide extra information to screen readers and other assistive technologies about what elements are, what state they're in, and what they do.

The Three Types of ARIA Attributes

// 1. ROLES — What is this element?
<div role="alert">Something went wrong!</div>
<div role="tablist">...</div>
<div role="dialog">...</div>
 
// 2. PROPERTIES — What are its characteristics?
<input aria-label="Search products" />
<div aria-describedby="help-text">...</div>
<button aria-haspopup="menu">Options</button>
 
// 3. STATES — What's happening right now?
<button aria-expanded={isOpen}>Menu</button>
<div aria-hidden={!isVisible}>...</div>
<option aria-selected={isSelected}>...</option>

The First Rule of ARIA: Don't Use ARIA

This sounds counterintuitive, but it's the most important rule. Use semantic HTML first. Native HTML elements come with built-in accessibility — keyboard support, screen reader announcements, focus management — all for free.

// ❌ ARIA to compensate for bad HTML
<div role="button" tabIndex={0} onClick={handleClick} onKeyDown={handleKeyDown}>
  Submit
</div>
 
// ✅ Just use a button — keyboard, focus, and screen reader support are free
<button onClick={handleClick}>Submit</button>
// ❌ ARIA for something HTML already does
<div role="navigation">
  <div role="list">
    <div role="listitem"><a href="/home">Home</a></div>
  </div>
</div>
 
// ✅ Semantic HTML — screen readers already know what these are
<nav>
  <ul>
    <li><a href="/home">Home</a></li>
  </ul>
</nav>

When You SHOULD Use ARIA

Use ARIA when you're building something that doesn't exist as a native HTML element:

  • Custom dropdowns / comboboxes
  • Tab panels
  • Modals / dialogs
  • Tree views
  • Toast notifications
  • Drag and drop interfaces
  • Progress indicators (custom ones)
// Custom tabs — no native HTML element for this pattern
<div role="tablist" aria-label="Account settings">
  <button role="tab" aria-selected={activeTab === 0} aria-controls="panel-0">
    Profile
  </button>
  <button role="tab" aria-selected={activeTab === 1} aria-controls="panel-1">
    Security
  </button>
</div>
 
<div role="tabpanel" id="panel-0" aria-labelledby="tab-0">
  Profile settings content...
</div>

Common ARIA Patterns You Must Know

PatternKey AttributesPurpose
Live regionaria-live="polite"Announce dynamic content changes
Dialogrole="dialog", aria-modal="true"Modal with focus trapping
Alertrole="alert"Urgent notification (auto-announced)
Expandedaria-expanded={true/false}Show/hide state of dropdown/accordion
Hiddenaria-hidden="true"Hide decorative elements from screen readers
Labelaria-label="..."Name elements without visible text
Described byaria-describedby="id"Link element to its description

aria-live: Announcing Dynamic Changes

Screen readers don't automatically notice when content changes on the page. aria-live fixes this:

function SearchResults({ count }: { count: number }) {
  return (
    <div aria-live="polite" aria-atomic="true">
      {count} results found
    </div>
  );
}
 
function ErrorToast({ message }: { message: string | null }) {
  return (
    <div role="alert">
      {message}
    </div>
  );
}
  • polite: Waits until the user is idle to announce. Use for search results, status updates.
  • assertive: Interrupts immediately. Use for errors and urgent alerts.
  • role="alert": Shorthand for aria-live="assertive".

The One-Liner That Impresses: "The first rule of ARIA is don't use ARIA — semantic HTML gives you accessibility for free. ARIA exists to fill gaps for custom widgets that have no native HTML equivalent, like comboboxes, tab panels, and live regions."

Common Follow-Up Questions

"What's the difference between aria-label and aria-labelledby?"

"aria-label provides an inline text label: aria-label='Close dialog'. aria-labelledby points to another element's ID whose text serves as the label: aria-labelledby='heading-1'. Use aria-labelledby when a visible label already exists — it avoids duplication and stays in sync."

"What does aria-hidden='true' do?"

"It hides an element from the accessibility tree — screen readers skip it completely. Use it for decorative icons, background images, or visual-only elements. Never use it on interactive elements or content that conveys meaning."

"Can ARIA override semantic HTML?"

"Yes, and that's dangerous. Adding role='button' to a <div> doesn't give it keyboard support — it just tells the screen reader it's a button. The user hears 'button' but pressing Enter does nothing. Bad ARIA is worse than no ARIA because it creates false expectations."

Red Flags in Your Answer

  • Using ARIA to fix bad HTML instead of using semantic HTML
  • Not knowing that role="button" on a div doesn't add keyboard support
  • Forgetting about aria-live for dynamic content updates
  • Adding aria-hidden="true" to content that sighted users can see and interact with
  • Not mentioning the "first rule of ARIA" (use HTML first)

Q30: How Do You Make an Autocomplete Accessible?

Interview Question: "Build an accessible autocomplete / combobox. What ARIA roles and keyboard interactions does it need?"

Think of it like a helpful assistant at a store. You start typing what you're looking for, the assistant suggests options, you can browse through them with arrow keys (like walking down a shelf), and press Enter to pick one. A sighted user sees this visually. An accessible autocomplete makes a screen reader user hear the same experience — "Suggestion 1 of 5: React hooks" — without seeing the dropdown.

The ARIA Pattern: Combobox

The WAI-ARIA combobox pattern has specific roles and properties:

function Autocomplete({ options, onSelect }: AutocompleteProps) {
  const [query, setQuery] = useState('');
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const listboxId = useId();
  const inputRef = useRef<HTMLInputElement>(null);
 
  const filtered = options.filter(opt =>
    opt.label.toLowerCase().includes(query.toLowerCase())
  );
 
  function handleKeyDown(e: React.KeyboardEvent) {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setIsOpen(true);
        setActiveIndex(prev => Math.min(prev + 1, filtered.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex(prev => Math.max(prev - 1, 0));
        break;
      case 'Enter':
        e.preventDefault();
        if (activeIndex >= 0 && filtered[activeIndex]) {
          onSelect(filtered[activeIndex]);
          setQuery(filtered[activeIndex].label);
          setIsOpen(false);
        }
        break;
      case 'Escape':
        setIsOpen(false);
        setActiveIndex(-1);
        inputRef.current?.focus();
        break;
    }
  }
 
  const activeDescendantId = activeIndex >= 0 ? `option-${activeIndex}` : undefined;
 
  return (
    <div>
      <label htmlFor="search-input">Search</label>
      <input
        id="search-input"
        ref={inputRef}
        role="combobox"
        aria-expanded={isOpen}
        aria-controls={listboxId}
        aria-activedescendant={activeDescendantId}
        aria-autocomplete="list"
        value={query}
        onChange={e => {
          setQuery(e.target.value);
          setIsOpen(true);
          setActiveIndex(-1);
        }}
        onKeyDown={handleKeyDown}
      />
 
      {isOpen && filtered.length > 0 && (
        <ul id={listboxId} role="listbox" aria-label="Search suggestions">
          {filtered.map((option, index) => (
            <li
              key={option.id}
              id={`option-${index}`}
              role="option"
              aria-selected={index === activeIndex}
              onClick={() => {
                onSelect(option);
                setQuery(option.label);
                setIsOpen(false);
              }}
            >
              {option.label}
            </li>
          ))}
        </ul>
      )}
 
      <div aria-live="polite" className="sr-only">
        {isOpen && filtered.length > 0
          ? `${filtered.length} suggestions available`
          : isOpen && query
            ? 'No suggestions found'
            : ''}
      </div>
    </div>
  );
}

Breaking Down the Key ARIA Attributes

AttributeOnPurpose
role="combobox"InputTells screen reader "this is a combo box with a popup"
aria-expandedInput"The suggestions list is open/closed"
aria-controlsInput"This input controls that list (by ID)"
aria-activedescendantInput"The currently highlighted option is this one" — focus stays on input
aria-autocomplete="list"Input"I suggest options as you type"
role="listbox"List"I'm a list of selectable options"
role="option"Each item"I'm one selectable option"
aria-selectedEach item"I'm the currently highlighted option"

The Keyboard Interactions

KeyAction
Arrow DownMove to next suggestion (open list if closed)
Arrow UpMove to previous suggestion
EnterSelect the highlighted suggestion
EscapeClose the suggestion list, return focus to input
TabAccept current selection and move to next form field

Why aria-activedescendant Instead of Moving Focus

This is the key insight. In a combobox, focus stays on the input while the user arrows through options. The screen reader knows which option is "active" through aria-activedescendant — it reads the option aloud without moving keyboard focus away from the input. This means the user can keep typing at any time.

The Live Region for Screen Reader Announcements

<div aria-live="polite" className="sr-only">
  {filtered.length} suggestions available
</div>

Without this, a screen reader user types and hears... nothing. They don't know suggestions appeared. The live region announces "5 suggestions available" so the user knows to press Arrow Down.

The One-Liner That Impresses: "An accessible combobox keeps focus on the input while using aria-activedescendant to virtually focus list items — the screen reader announces each option without moving the cursor, and a live region tells the user how many suggestions appeared."

Common Follow-Up Questions

"How do you handle async loading in an accessible autocomplete?"

"Add a loading state with aria-busy='true' on the listbox and announce 'Loading suggestions...' in the live region. When results arrive, update the live region with the count. If loading takes more than a second, show a visible loading indicator too."

"What if the autocomplete allows free text AND selection?"

"That's the default combobox behavior — the user can type anything OR select from suggestions. Set aria-autocomplete='list' (suggests but doesn't force) vs aria-autocomplete='both' (input also auto-completes the text). The key difference is whether Tab accepts a suggestion or just moves to the next field."

"How do you test accessibility?"

"Three layers: (1) Automated — axe-core or lighthouse audits catch ~30% of issues. (2) Keyboard — navigate the entire feature without a mouse. (3) Screen reader — test with VoiceOver (Mac) or NVDA (Windows) at least for critical flows. Automated testing alone is not enough."

Red Flags in Your Answer

  • Building a "dropdown" with divs and no ARIA roles
  • Moving focus to list items instead of using aria-activedescendant
  • Forgetting the live region announcement for screen readers
  • No keyboard navigation (Arrow keys, Enter, Escape)
  • Not mentioning that the input needs role="combobox"

Bonus: How to Make Forms Accessible

Interview Question: "What makes a form accessible?"

Think of it like filling out a paper form vs a phone form. On paper, every field has a label right next to it, required fields have asterisks, and errors are written in red next to the problem. An accessible digital form does the same thing — but also works for people who can't see it.

Labels: The Foundation

Every input MUST have a label. No exceptions.

// ✅ Explicit label — connected by htmlFor/id
<label htmlFor="email">Email address</label>
<input id="email" type="email" required />
 
// ✅ Implicit label — input wrapped inside label
<label>
  Email address
  <input type="email" required />
</label>
 
// ❌ Placeholder is NOT a label — disappears when you type
<input type="email" placeholder="Email address" />
 
// ❌ aria-label as a last resort — no visible label for sighted users
<input type="email" aria-label="Email address" />

Error Messages: Connected and Announced

function FormField({ label, error, children, id }: FormFieldProps) {
  const errorId = `${id}-error`;
 
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      {React.cloneElement(children, {
        id,
        'aria-invalid': !!error,
        'aria-describedby': error ? errorId : undefined,
      })}
      {error && (
        <p id={errorId} role="alert" className="text-red-600">
          {error}
        </p>
      )}
    </div>
  );
}
 
// Usage
<FormField id="email" label="Email" error={errors.email}>
  <input type="email" />
</FormField>

The key attributes:

  • aria-invalid: Tells screen readers "this field has an error"
  • aria-describedby: Links the input to its error message — screen reader reads both the label AND the error
  • role="alert": Announces the error immediately when it appears

Focus Management on Errors

When form submission fails, move focus to the first error:

function handleSubmit(e: React.FormEvent) {
  e.preventDefault();
  const validationErrors = validate(formData);
 
  if (Object.keys(validationErrors).length > 0) {
    setErrors(validationErrors);
    const firstErrorField = Object.keys(validationErrors)[0];
    document.getElementById(firstErrorField)?.focus();
  }
}

Required Fields

// Both visual and programmatic indication
<label htmlFor="name">
  Name <span aria-hidden="true">*</span>
</label>
<input id="name" required aria-required="true" />

The required attribute provides browser validation. aria-required communicates to screen readers. The visual asterisk is hidden from screen readers with aria-hidden because the screen reader already knows it's required.

The One-Liner That Impresses: "An accessible form connects every input to its label, links errors with aria-describedby, announces validation failures with role='alert', and moves focus to the first error on submission — it's the same UX sighted users get, translated to sound."


Bonus: Keyboard Navigation Patterns

Interview Question: "What keyboard patterns should a React app support?"

The Universal Patterns

PatternKeysBehavior
Tab navigationTab / Shift+TabMove between interactive elements
Button activationEnter or SpaceActivate a button
Link navigationEnterFollow a link
Modal closeEscapeClose the topmost modal
DropdownArrow Up/DownNavigate options
TabsArrow Left/RightSwitch between tabs
MenuArrow Down then EnterOpen and select

Focus Trapping in Modals

When a modal opens, keyboard focus must be trapped inside it. Tab should cycle through modal elements and never escape to the page behind:

function Modal({ isOpen, onClose, children }: ModalProps) {
  const modalRef = useRef<HTMLDivElement>(null);
 
  useEffect(() => {
    if (!isOpen) return;
 
    const modal = modalRef.current;
    if (!modal) return;
 
    const focusableElements = modal.querySelectorAll<HTMLElement>(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    const first = focusableElements[0];
    const last = focusableElements[focusableElements.length - 1];
 
    first?.focus();
 
    function handleKeyDown(e: KeyboardEvent) {
      if (e.key === 'Escape') { onClose(); return; }
      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();
      }
    }
 
    modal.addEventListener('keydown', handleKeyDown);
    return () => modal.removeEventListener('keydown', handleKeyDown);
  }, [isOpen, onClose]);
 
  if (!isOpen) return null;
 
  return (
    <div role="dialog" aria-modal="true" ref={modalRef}>
      {children}
    </div>
  );
}

Focus Restoration

When a modal closes, focus should return to the element that opened it:

function useModalFocusReturn() {
  const triggerRef = useRef<HTMLElement | null>(null);
 
  function openModal() {
    triggerRef.current = document.activeElement as HTMLElement;
  }
 
  function closeModal() {
    triggerRef.current?.focus();
  }
 
  return { openModal, closeModal };
}

Skip Navigation Link

For keyboard users, tabbing through the entire nav on every page is painful. A skip link lets them jump to main content:

function SkipLink() {
  return (
    <a
      href="#main-content"
      className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-white focus:p-2"
    >
      Skip to main content
    </a>
  );
}
 
function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <SkipLink />
      <nav>...</nav>
      <main id="main-content" tabIndex={-1}>
        {children}
      </main>
    </>
  );
}

Visible Focus Indicators

Never remove focus outlines without providing an alternative:

// ❌ Removes focus indicator — keyboard users can't see where they are
button:focus { outline: none; }
 
// ✅ Custom focus indicator — visible and styled
button:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

focus-visible only shows the outline for keyboard navigation, not mouse clicks. Best of both worlds.

The One-Liner That Impresses: "Keyboard accessibility means every interactive element is reachable with Tab, operable with Enter/Space, and dismissible with Escape — with focus trapped in modals, restored on close, and always visibly indicated."

Red Flags in Your Answer

  • Saying "accessibility is nice to have" instead of treating it as a core requirement
  • Removing focus outlines without a replacement
  • Not mentioning focus trapping in modals
  • Forgetting focus restoration when modals/popups close
  • Only talking about screen readers without mentioning keyboard-only users
  • Never having tested with a screen reader