Keyboard focus management patterns for accessibility. Covers focus traps, roving tabindex, focus restore, skip links, and FocusScope components for WCAG-compliant interactive widgets. Use when implementing focus traps or keyboard navigation.
View on GitHubyonatangross/orchestkit
ork
January 25, 2026
Select agents to install to:
npx add-skill https://github.com/yonatangross/orchestkit/blob/main/plugins/ork/skills/focus-management/SKILL.md -a claude-code --skill focus-managementInstallation paths:
.claude/skills/focus-management/# Focus Management
Essential patterns for managing keyboard focus in accessible web applications, ensuring keyboard-only users can navigate complex interactive components.
## Overview
- Building modals, dialogs, or drawers that require focus trapping
- Implementing tab panels, menus, or toolbars with roving tabindex
- Restoring focus after closing overlays or completing actions
- Creating skip links for keyboard navigation
- Ensuring focus visibility meets WCAG 2.4.7 requirements
## Quick Reference
### FocusScope Trap (React Aria)
```tsx
import { FocusTrap } from '@react-aria/focus';
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return (
<div role="dialog" aria-modal="true">
<FocusTrap>
<div className="modal-content">
{children}
<button onClick={onClose}>Close</button>
</div>
</FocusTrap>
</div>
);
}
```
### Roving Tabindex
```tsx
function TabList({ tabs, onSelect }) {
const [activeIndex, setActiveIndex] = useState(0);
const tabRefs = useRef<HTMLButtonElement[]>([]);
const handleKeyDown = (e: KeyboardEvent, index: number) => {
const keyMap: Record<string, number> = {
ArrowRight: (index + 1) % tabs.length,
ArrowLeft: (index - 1 + tabs.length) % tabs.length,
Home: 0, End: tabs.length - 1,
};
if (e.key in keyMap) {
e.preventDefault();
setActiveIndex(keyMap[e.key]);
tabRefs.current[keyMap[e.key]]?.focus();
}
};
return (
<div role="tablist">
{tabs.map((tab, i) => (
<button key={tab.id} ref={(el) => (tabRefs.current[i] = el!)}
role="tab" tabIndex={i === activeIndex ? 0 : -1}
aria-selected={i === activeIndex}
onKeyDown={(e) => handleKeyDown(e, i)}
onClick={() => { setActiveIndex(i); onSelect(tab); }}>
{tab.label}
</button>
))}
</div>
);
}
```
### Focus Restore
```tsx
function useRestoreFocus(isOpen: boolean) {
const trigge