React Curriculum

Machine Coding: Accessible Tabs Compound Component

Medium100% Free~35 mins#react#machine-coding#tabs#compound-components#accessibility#keyboard
Interview Answer (30-Second Summary)

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';
2
3interface TabsContextValue {
4 activeIndex: number;
5 setActiveIndex: (idx: number) => void;
6 tabCount: number;
7 registerTab: () => number;
8}
9
10const TabsContext = createContext<TabsContextValue | null>(null);
11
12function useTabs() {
13 const ctx = useContext(TabsContext);
14 if (!ctx) throw new Error('Tabs compound subcomponents must be rendered inside <Tabs>');
15 return ctx;
16}
17
18// 1. Root Tabs Container
19export 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);
28
29 const registerTab = () => {
30 const idx = tabCounter.current;
31 tabCounter.current += 1;
32 return idx;
33 };
34
35 return (
36 <TabsContext.Provider value={{ activeIndex, setActiveIndex, tabCount: tabCounter.current, registerTab }}>
37 <div className="tabs-root">{children}</div>
38 </TabsContext.Provider>
39 );
40}
41
42// 2. TabList with Keyboard Navigation
43export function TabList({ children }: { children: ReactNode }) {
44 const { activeIndex, setActiveIndex } = useTabs();
45 const listRef = useRef<HTMLDivElement>(null);
46
47 const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
48 const tabs = listRef.current?.querySelectorAll<HTMLButtonElement>('[role="tab"]');
49 if (!tabs || tabs.length === 0) return;
50
51 let newIndex = activeIndex;
52
53 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 }
73
74 setActiveIndex(newIndex);
75 tabs[newIndex]?.focus();
76 };
77
78 return (
79 <div ref={listRef} role="tablist" onKeyDown={handleKeyDown} className="tab-list">
80 {children}
81 </div>
82 );
83}
84
85// 3. Tab Button with Roving tabindex
86export function Tab({ index, children }: { index: number; children: ReactNode }) {
87 const { activeIndex, setActiveIndex } = useTabs();
88 const isSelected = activeIndex === index;
89
90 return (
91 <button
92 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}
102
103// 4. TabPanel
104export function TabPanel({ index, children }: { index: number; children: ReactNode }) {
105 const { activeIndex } = useTabs();
106 if (activeIndex !== index) return null;
107
108 return (
109 <div role="tabpanel" tabIndex={0} className="tab-panel">
110 {children}
111 </div>
112 );
113}
Deep Architectural Explanation

Architecture of Compound Accessible Tabs

  1. 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.
  2. 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).
  3. 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
What is the compound components pattern for tabs?

Composing Tabs, TabList, Tab, and TabPanel components that communicate implicitly through React Context, giving developers total flexibility over styling and layout.

It allows keyboard users to navigate between tabs with Arrow keys while pressing Tab jumps directly into the tabpanel content, avoiding repetitive keystrokes.

role="tablist" on the tab button wrapper, role="tab" with aria-selected on each tab button, and role="tabpanel" on each content section.

Accept optional index and onChange props. If passed, use controlled state; otherwise, fall back to internal useState initialized with defaultIndex.

If the tabpanel contains scrollable content without focusable interactive controls, tabIndex={0} ensures keyboard users can scroll the panel using arrow keys.

Yes, by having Tab components register themselves in a Context array on mount or generating deterministic IDs using React.useId.