React Curriculum

Accessible Components: Keyboard Nav & Focus Management

Medium100% Free~25 mins#react#accessibility#a11y#aria#focus-management#keyboard
Interview Answer (30-Second Summary)

Accessible React components comply with WAI-ARIA authoring practices by utilizing semantic HTML, proper ARIA states (aria-expanded, aria-selected), managing keyboard events (Arrow navigation, Escape, Enter), and programmatic focus management via useRef and focus() to guarantee screen reader and keyboard parity.

Key Learning Objectives
✓

Implement WAI-ARIA attributes (aria-expanded, aria-controls, aria-haspopup, role) correctly in React.

✓

Manage focus programmatically using useRef and .focus() on component mount, toggle, and dismiss.

✓

Build keyboard roving tabindex patterns for accessible menus and radio groups.

✓

Restore focus to the opening trigger element when overlays or dropdowns close.

The Interview Problem & Context

Build an accessible custom Dropdown Menu component that supports keyboard navigation (Escape, ArrowUp, ArrowDown), announces states with ARIA, and restores focus on close.

1import React, { useState, useRef, useEffect, KeyboardEvent } from 'react';
2
3interface DropdownProps {
4 label: string;
5 items: Array<{ id: string; label: string; onSelect: () => void }>;
6}
7
8export function AccessibleDropdown({ label, items }: DropdownProps) {
9 const [isOpen, setIsOpen] = useState(false);
10 const [activeIndex, setActiveIndex] = useState(0);
11
12 const triggerRef = useRef<HTMLButtonElement>(null);
13 const menuRef = useRef<HTMLUListElement>(null);
14
15 // 1. Focus active item when menu opens
16 useEffect(() => {
17 if (isOpen) {
18 const activeEl = menuRef.current?.children[activeIndex] as HTMLElement;
19 activeEl?.focus();
20 } else {
21 // Restore focus to trigger button when closed
22 triggerRef.current?.focus();
23 }
24 }, [isOpen]);
25
26 // 2. Keyboard Navigation
27 const handleKeyDown = (e: KeyboardEvent) => {
28 if (!isOpen) {
29 if (['ArrowDown', 'Enter', ' '].includes(e.key)) {
30 e.preventDefault();
31 setIsOpen(true);
32 }
33 return;
34 }
35
36 switch (e.key) {
37 case 'ArrowDown':
38 e.preventDefault();
39 setActiveIndex((prev) => (prev + 1) % items.length);
40 break;
41 case 'ArrowUp':
42 e.preventDefault();
43 setActiveIndex((prev) => (prev - 1 + items.length) % items.length);
44 break;
45 case 'Escape':
46 e.preventDefault();
47 setIsOpen(false);
48 break;
49 case 'Enter':
50 case ' ':
51 e.preventDefault();
52 items[activeIndex].onSelect();
53 setIsOpen(false);
54 break;
55 }
56 };
57
58 return (
59 <div className="dropdown-container" onKeyDown={handleKeyDown}>
60 {/* Semantic button with full ARIA announcement */}
61 <button
62 ref={triggerRef}
63 aria-haspopup="menu"
64 aria-expanded={isOpen}
65 onClick={() => setIsOpen((prev) => !prev)}
66 className="dropdown-trigger"
67 >
68 {label}
69 </button>
70
71 {isOpen && (
72 <ul ref={menuRef} role="menu" className="dropdown-menu">
73 {items.map((item, index) => (
74 <li
75 key={item.id}
76 role="menuitem"
77 tabIndex={activeIndex === index ? 0 : -1} // Roving tabindex
78 onClick={() => {
79 item.onSelect();
80 setIsOpen(false);
81 }}
82 className={activeIndex === index ? 'focused' : ''}
83 >
84 {item.label}
85 </li>
86 ))}
87 </ul>
88 )}
89 </div>
90 );
91}
Deep Architectural Explanation

The Pillars of Accessible React Components

1. Semantic Markup & ARIA Contracts:

  • aria-haspopup="menu": Informs assistive technology that activating the button reveals a menu.
  • aria-expanded={isOpen}: Screen readers announce "expanded" or "collapsed" whenever state toggles.
  • role="menu" and role="menuitem": Defines the widget role hierarchy according to WAI-ARIA authoring practices.

2. Keyboard Parity:

A mouse user clicks; a keyboard user requires:

  • Enter / Space / ArrowDown: Opens the dropdown.
  • ArrowDown / ArrowUp: Moves focus between items with wrap-around.
  • Escape: Closes the dropdown.
  • Enter: Activates the selected item.

3. Focus Restoration:

When the menu closes, focus must return to triggerRef.current. If focus is dropped, the browser resets focus to document.body, forcing the user to tab through the entire website again!

4. Roving tabindex Pattern:

Only the currently active item has tabIndex={0}; all other items have tabIndex={-1}. This allows the user to press Tab to jump out of the menu in a single keystroke rather than tabbing through 20 items.

Common Interview Traps & Pitfalls
✕

Using `<div onClick={...}>` instead of a semantic `<button>`, losing keyboard focusability and Enter/Space activation.

✕

Failing to restore focus to the trigger button when an overlay or menu closes.

✕

Putting `tabIndex={0}` on every item in a dropdown, forcing keyboard users to tab through every single item.

Senior Interviewer Follow-Up Probes
?

How does the WAI-ARIA combobox pattern differ from a simple menu dropdown?

?

How do screen readers announce dynamic content updates using `aria-live="polite"` vs `aria-live="assertive"`?

?

What automated testing tools (e.g. axe-core, jest-axe) integrate with React Testing Library?

Frequently Asked Interview Questions
What is roving tabindex in accessibility?

A focus management technique where only one item in a composite widget (menu, tabs, grid) has tabIndex=0 (focusable via Tab), while all others have tabIndex=-1. Arrow keys move focus between items programmatically.

If focus is not restored to the originating trigger element, focus defaults to the document body or top of page, disorienting keyboard and screen reader users.

aria-label takes a direct string value for elements lacking visible text (like an icon button). aria-labelledby takes the ID of another visible DOM element that serves as the label.

Native <button> and <a> elements provide built-in focusability, keyboard triggers (Enter/Space), disabled states, and correct screen reader announcements without custom event listeners.

It communicates whether a collapsible element (like an accordion, dropdown, or drawer) is currently open (true) or closed (false).

Use jest-axe for automated unit tests, eslint-plugin-jsx-a11y for linting, Chrome Lighthouse / Axe DevTools for audits, and manual keyboard and VoiceOver/NVDA screen reader testing.