typescriptintermediate
Accessible Tabs Component
Build an accessible tabs component with keyboard navigation, ARIA attributes, and focus management.
typescriptPress ⌘/Ctrl + Shift + C to copy
import { useState, useRef, useCallback, KeyboardEvent } from 'react';
interface TabItem {
id: string;
label: string;
content: React.ReactNode;
disabled?: boolean;
}
function Tabs({ tabs, defaultTab }: { tabs: TabItem[]; defaultTab?: string }) {
const [activeTab, setActiveTab] = useState(defaultTab ?? tabs[0]?.id ?? '');
const tabRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
const enabledTabs = tabs.filter((t) => !t.disabled);
const focusTab = useCallback((id: string) => {
setActiveTab(id);
tabRefs.current.get(id)?.focus();
}, []);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
const currentIdx = enabledTabs.findIndex((t) => t.id === activeTab);
let nextIdx = currentIdx;
switch (e.key) {
case 'ArrowRight':
nextIdx = (currentIdx + 1) % enabledTabs.length;
break;
case 'ArrowLeft':
nextIdx = (currentIdx - 1 + enabledTabs.length) % enabledTabs.length;
break;
case 'Home':
nextIdx = 0;
break;
case 'End':
nextIdx = enabledTabs.length - 1;
break;
default:
return;
}
e.preventDefault();
focusTab(enabledTabs[nextIdx].id);
},
[activeTab, enabledTabs, focusTab],
);
const activeContent = tabs.find((t) => t.id === activeTab)?.content;
return (
<div>
<div role="tablist" className="flex gap-1 border-b border-white/10" onKeyDown={handleKeyDown}>
{tabs.map((tab) => (
<button
key={tab.id}
role="tab"
ref={(el) => { if (el) tabRefs.current.set(tab.id, el); }}
id={`tab-${tab.id}`}
aria-selected={activeTab === tab.id}
aria-controls={`panel-${tab.id}`}
aria-disabled={tab.disabled}
tabIndex={activeTab === tab.id ? 0 : -1}
onClick={() => !tab.disabled && setActiveTab(tab.id)}
className={`px-4 py-2 text-sm font-medium transition-colors
${
activeTab === tab.id
? 'text-blue-400 border-b-2 border-blue-400'
: tab.disabled
? 'text-gray-600 cursor-not-allowed'
: 'text-gray-400 hover:text-white'
}`}
>
{tab.label}
</button>
))}
</div>
<div
role="tabpanel"
id={`panel-${activeTab}`}
aria-labelledby={`tab-${activeTab}`}
className="py-4"
>
{activeContent}
</div>
</div>
);
}
// Usage:
// <Tabs tabs={[
// { id: 'code', label: 'Code', content: <CodeView /> },
// { id: 'preview', label: 'Preview', content: <PreviewPane /> },
// { id: 'tests', label: 'Tests', content: <TestResults />, disabled: true },
// ]} />
export { Tabs, type TabItem };Use Cases
- Tab navigation interfaces
- Settings panels
- Code/preview toggle
Tags
Related Snippets
Similar patterns you can reuse in the same workflow.
typescriptintermediate
React Portal Modal Component
Build an accessible modal dialog using React portals with focus trapping and keyboard handling.
Best for: Accessible modal dialogs with focus management
#react#portal
typescriptbeginner
useKeyboardShortcut Hook
Register global keyboard shortcuts with modifier keys, preventing defaults, and cleanup on unmount.
Best for: Command palette shortcuts
#react#hooks
typescriptintermediate
Error Boundary with Fallback UI
Class-based error boundary component that catches render errors and displays a customizable fallback UI.
Best for: Graceful error recovery
#error-boundary#error-handling
typescriptintermediate
Portal-Based Modal Component
Accessible modal component using React portals with focus trapping, Escape key close, and backdrop click.
Best for: Confirmation dialogs
#modal#portal