- React
- Live Machine Coding Challenges and Custom Hooks
- Machine Coding: Accessible Tabs Compound Component
Machine Coding: Accessible Tabs Compound Component
The Accessible Tabs compound component shares active tab state via React Context between Tabs, TabList, Tab, and TabPanel. It adheres to WAI-ARIA Tabs pattern using role="tablist", role="tab", role="tabpanel", aria-selected, and keyboard arrow key navigation with roving tabindex.
Key Learning Objectives
Build a compound Tabs component (<Tabs>, <TabList>, <Tab>, <TabPanels>, <TabPanel>).
Share active tab index and tab switching callbacks via scoped React Context.
Implement WAI-ARIA tab semantics (role="tablist", role="tab", role="tabpanel", aria-selected, aria-controls).
Implement keyboard navigation (ArrowLeft, ArrowRight, Home, End) with automatic/manual focus activation.
The Interview Problem & Context
Implement an accessible, compound `<Tabs>` component with keyboard ArrowLeft/ArrowRight navigation, ARIA tab roles, and flexible child composition.
1import React, { createContext, useContext, useState, useRef, KeyboardEvent, ReactNode } from 'react';23interface TabsContextValue {4 activeIndex: number;5 setActiveIndex: (idx: number) => void;6 tabCount: number;7 registerTab: () => number;8}910const TabsContext = createContext<TabsContextValue | null>(null);1112function useTabs() {13 const ctx = useContext(TabsContext);14 if (!ctx) throw new Error('Tabs compound subcomponents must be rendered inside <Tabs>');15 return ctx;16}1718// 1. Root Tabs Container19export function Tabs({20 defaultIndex = 0,21 children,22}: {23 defaultIndex?: number;24 children: ReactNode;25}) {26 const [activeIndex, setActiveIndex] = useState(defaultIndex);27 const tabCounter = useRef(0);2829 const registerTab = () => {30 const idx = tabCounter.current;31 tabCounter.current += 1;32 return idx;33 };3435 return (36 <TabsContext.Provider value={{ activeIndex, setActiveIndex, tabCount: tabCounter.current, registerTab }}>37 <div className="tabs-root">{children}</div>38 </TabsContext.Provider>39 );40}4142// 2. TabList with Keyboard Navigation43export function TabList({ children }: { children: ReactNode }) {44 const { activeIndex, setActiveIndex } = useTabs();45 const listRef = useRef<HTMLDivElement>(null);4647 const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {48 const tabs = listRef.current?.querySelectorAll<HTMLButtonElement>('[role="tab"]');49 if (!tabs || tabs.length === 0) return;5051 let newIndex = activeIndex;5253 switch (e.key) {54 case 'ArrowRight':55 e.preventDefault();56 newIndex = (activeIndex + 1) % tabs.length;57 break;58 case 'ArrowLeft':59 e.preventDefault();60 newIndex = (activeIndex - 1 + tabs.length) % tabs.length;61 break;62 case 'Home':63 e.preventDefault();64 newIndex = 0;65 break;66 case 'End':67 e.preventDefault();68 newIndex = tabs.length - 1;69 break;70 default:71 return;72 }7374 setActiveIndex(newIndex);75 tabs[newIndex]?.focus();76 };7778 return (79 <div ref={listRef} role="tablist" onKeyDown={handleKeyDown} className="tab-list">80 {children}81 </div>82 );83}8485// 3. Tab Button with Roving tabindex86export function Tab({ index, children }: { index: number; children: ReactNode }) {87 const { activeIndex, setActiveIndex } = useTabs();88 const isSelected = activeIndex === index;8990 return (91 <button92 role="tab"93 aria-selected={isSelected}94 tabIndex={isSelected ? 0 : -1}95 onClick={() => setActiveIndex(index)}96 className={`tab-button ${isSelected ? 'active' : ''}`}97 >98 {children}99 </button>100 );101}102103// 4. TabPanel104export function TabPanel({ index, children }: { index: number; children: ReactNode }) {105 const { activeIndex } = useTabs();106 if (activeIndex !== index) return null;107108 return (109 <div role="tabpanel" tabIndex={0} className="tab-panel">110 {children}111 </div>112 );113}
Deep Architectural Explanation
Architecture of Compound Accessible Tabs
- State Partitioning via Context:
- activeIndex is held by the root <Tabs> provider.
- Any subcomponent can read or set the active tab without explicit prop drilling.
- WAI-ARIA Pattern Compliance:
- Container has role="tablist".
- Buttons have role="tab" and aria-selected={isSelected}.
- Content containers have role="tabpanel" and tabIndex={0} (so keyboard users can scroll panel content).
- Roving tabindex & Arrow Navigation:
- Only the currently active tab has tabIndex={0}.
- All inactive tabs have tabIndex={-1}.
- When a user presses Tab into the tablist, focus lands exclusively on the active tab. Pressing Tab again moves past the tablist into the tabpanel!
- Arrow keys move focus horizontally between tabs, automatically updating the active panel.
Common Interview Traps & Pitfalls
Leaving tabIndex={0} on all tabs, forcing keyboard users to tab through every single tab button before reaching content.
Forgetting role="tabpanel" on content containers, leaving screen readers unaware of the content role.
Passing active state down through props, creating rigid component hierarchies that break when consumers add wrapper divs.
Senior Interviewer Follow-Up Probes
How would you support vertical tabs with ArrowUp and ArrowDown keys?
How can tab panels be animated smoothly using CSS grid and transitions?
How do you support asynchronous tab content loading with Suspense?
Frequently Asked Interview Questions
Why does roving tabindex improve accessibility in tablists?
It allows keyboard users to navigate between tabs with Arrow keys while pressing Tab jumps directly into the tabpanel content, avoiding repetitive keystrokes.
What roles are required by WAI-ARIA for accessible tabs?
role="tablist" on the tab button wrapper, role="tab" with aria-selected on each tab button, and role="tabpanel" on each content section.
How do you handle controlled vs uncontrolled tabs?
Accept optional index and onChange props. If passed, use controlled state; otherwise, fall back to internal useState initialized with defaultIndex.
Why should TabPanel have tabIndex={0}?
If the tabpanel contains scrollable content without focusable interactive controls, tabIndex={0} ensures keyboard users can scroll the panel using arrow keys.
Can you dynamically register tab indices automatically?
Yes, by having Tab components register themselves in a Context array on mount or generating deterministic IDs using React.useId.
