a11y: implement WAI-ARIA Tabs pattern on tab navigation#65
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces WAI-ARIA compliant keyboard navigation and accessibility attributes to the tabs and tab panel in Panel.tsx. The review feedback highlights two important improvements: first, removing aria-live="polite" from the main tab panel to prevent screen readers from being overwhelmed by real-time debugger updates, and second, replacing the global document.getElementById call with a scoped query selector to ensure the component remains safe and robust when multiple instances are rendered.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| <main | ||
| className="tab-content" | ||
| role="tabpanel" | ||
| id={`panel-${activeTab}`} | ||
| aria-labelledby={`tab-${activeTab}`} | ||
| aria-live="polite" | ||
| tabIndex={0} | ||
| > |
There was a problem hiding this comment.
Applying aria-live="polite" to the entire <main> tab panel is an accessibility anti-pattern.
Because this is a debugger panel with real-time streaming data (such as timeline events, performance metrics, and memory updates), any background update will trigger continuous screen reader announcements. This will overwhelm screen reader users and make the tool extremely noisy and difficult to use.
aria-live should only be used on small, specific regions (like a status message or toast notification) rather than large content containers. Please remove aria-live="polite" from the main tab panel.
| <main | |
| className="tab-content" | |
| role="tabpanel" | |
| id={`panel-${activeTab}`} | |
| aria-labelledby={`tab-${activeTab}`} | |
| aria-live="polite" | |
| tabIndex={0} | |
| > | |
| <main | |
| className="tab-content" | |
| role="tabpanel" | |
| id={`panel-${activeTab}`} | |
| aria-labelledby={`tab-${activeTab}`} | |
| tabIndex={0} | |
| > |
| const nextBtn = document.getElementById(`tab-${nextTab.id}`); | ||
| nextBtn?.focus(); |
There was a problem hiding this comment.
Using global document.getElementById inside a React component is a code smell and can lead to bugs if multiple instances of this panel are rendered (e.g., in testing environments or multiple DevTools windows). Scoping the query to the component's subtree is safer and more idiomatic.
Since the event handler is attached directly to the tab buttons, you can leverage e.currentTarget.parentElement to query only within the parent <nav> container.
| const nextBtn = document.getElementById(`tab-${nextTab.id}`); | |
| nextBtn?.focus(); | |
| const nextBtn = (e.currentTarget as HTMLElement).parentElement?.querySelector<HTMLButtonElement>(`#tab-${nextTab.id}`); | |
| nextBtn?.focus(); |
hoainho
left a comment
There was a problem hiding this comment.
Thanks @Kunall7890! This is a clean, spec-compliant WAI-ARIA Tabs implementation.
A few observations from review:
Correct:
role=\tablist\+aria-labelon<nav>✅role=\tab\witharia-selected+aria-controlson each button ✅- Roving
tabIndex(0/-1) ✅ - ArrowRight/Left/Home/End key handlers ✅
role=\tabpanel\witharia-labelledbyon<main>✅aria-labelon badge counts ✅- Build passes, no new deps ✅
Minor (not blocking):
aria-live=\polite\on tabpanel — in a debugging panel where content updates can be frequent, this may announce too aggressively. If users report 'chattiness', we can switch toaria-live=\off\(inert) and rely on the focus management alone. Let's keep it for now.document.getElementByIdvs scoped ref — safe here since there's exactly one Panel instance per DevTools window, but worth noting for future component extraction.
Great work as always! 🚀
Linked issue
Closes #28
Type of change
What changed
Implemented the WAI-ARIA Tabs pattern on the main tab navigation in
Panel.tsx. Only one file changed.
and roving tabIndex (0 for active, -1 for inactive)
focus between tabs per the WAI-ARIA Tabs spec
Testing notes
$ npm run build
(succeeds with no errors)
$ grep -n "role="tablist"" src/panel/Panel.tsx
452:
$ grep -n "aria-selected" src/panel/Panel.tsx
460: aria-selected={activeTab === tab.id}
$ grep -n "role="tabpanel"" src/panel/Panel.tsx
478: role="tabpanel"
$ grep -n "aria-live" src/panel/Panel.tsx
481: aria-live="polite"
Keyboard tested:
Claim confirmation
Checklist
Additional notes
aria-live="polite" is set on the tabpanel so screen readers announce
new Timeline events as they arrive without interrupting the user.
Badge counts on tabs have aria-label="X items" so screen readers
announce the number meaningfully instead of just reading the digit.