Focus management patterns
Focus management patterns define how keyboard navigation and focus behavior work across components. These patterns ensure accessible, predictable interactions for all users, including those who rely on keyboard navigation.
Component patterns
Modal dialog
Behavior:
- Focus traps within dialog
- Focus moves to first interactive element on open
- Escape closes dialog
- Focus returns to trigger element on close
Common mistakes:
- Not trapping focus, allowing Tab to exit modal
- Not restoring focus to trigger on close
- Missing
aria-modal="true"
Alert dialog (critical)
For destructive or critical confirmations where Escape should NOT close.
Key difference: Focus the LEAST destructive option (Cancel) by default.
Dropdown menu
Keyboard navigation:
- ↑/↓: move between items (wraps at boundaries)
- Enter / Space: activate item
- Escape: close menu
- Tab: exit menu (does NOT navigate items)
- Home / End: jump to first/last item
- Type character: jump to item starting with that letter
Tabs
{tabs.map((tab, index) => (
))}
{tabs.map((tab, index) => (
{tab.content}
))}
Keyboard navigation:
- ←/→: move between tabs (wraps)
- Usually automatic activation (arrow changes tab)
- Tab key: exit tablist to panel content
Toolbar
{tools.map((tool, index) => (
))}
Key behavior: Toolbar is ONE tab stop. Use arrow keys to move between items.
Combobox / select
{isOpen && (
{options.map((option) => (
-
{option.label}
))}
)}
Key difference: Arrow keys do NOT wrap: stop at first/last option.
Skip links
Skip to main content
{/* Later in the page */}
{/* Content */}
Requirements:
- Must be first focusable element on page
- Hidden until focused
- Target needs
tabIndex="-1"to receive programmatic focus
Focus rules
Visibility rules
| Rule | Requirement | WCAG |
|---|---|---|
| Visible focus indicator | All interactive elements must show focus | 2.4.7 (AA) |
| Use focus-visible | Show focus only on keyboard nav, not clicks | Best practice |
| Focus contrast | Focus indicator must have 3:1 contrast | 2.4.11 (AAA) |
// Good — visible focus
className="focus:ring-2 focus:ring-offset-2"
className="focus-visible:ring-2 focus-visible:ring-offset-2"
// Bad — hidden focus
className="focus:outline-none" // Without replacement
Order rules
| Rule | Requirement | WCAG |
|---|---|---|
| Logical tab order | Must follow visual / reading order | 2.4.3 (A) |
| No positive tabindex | Never use tabindex="1", "2", etc. | 2.4.3 (A) |
| Skip links first | Skip links must be first focusable | 2.4.1 (A) |
// Good
tabIndex="0" // Focusable in natural order
tabIndex="-1" // Programmatic focus only
// Bad
tabIndex="5" // Creates unpredictable order
tabIndex="99" // Maintenance nightmare
Focus trap rules
| Rule | Requirement | WCAG |
|---|---|---|
| Modal focus trap | Focus must cycle within modal | 2.4.3 (A) |
| Focus return | Return focus to trigger on close | 2.4.3 (A) |
| Escape closes | Escape should close overlays (except critical) | Best practice |
Landmark regions
Standard landmark regions for skip link navigation:
| Name | Role | HTML Element | Skip Link |
|---|---|---|---|
| main | main | <main> | "Skip to main content" |
| navigation | navigation | <nav> | "Skip to navigation" |
| search | search | <search> | "Skip to search" |
| banner | banner | <header> | : |
| contentinfo | contentinfo | <footer> | : |
MCP tool usage
Get focus pattern
// Get patterns for modal components
get_focus_pattern({ componentType: 'modal' })
// Get specific pattern
get_focus_pattern({ name: 'alert-dialog' })
// Include rules and landmarks
get_focus_pattern({
componentType: 'modal',
includeRules: true,
includeLandmarks: true
})
Validate focus management
// Basic validation
validate_focus_management({
code: ''
})
// With component type context
validate_focus_management({
code: '',
componentType: 'dropdown'
})
// Strict mode (warnings become errors)
validate_focus_management({
code: '...',
strict: true
})
Validation checks
| Issue | Severity | Fix |
|---|---|---|
| Missing focus indicator | Error | Add focus:ring-2 or equivalent |
| Positive tabindex | Error | Use tabindex="0" or -1 |
| Modal without focus trap | Error | Use focus trap library + aria-modal |
| Dialog missing aria-modal | Warning | Add aria-modal="true" |
| Tab missing aria-selected | Error | Add aria-selected attribute |
| Click without keyboard | Warning | Add role="button" + keyboard handler |
| autoFocus usage | Warning | Ensure intentional and appropriate |
Quick reference
Keyboard navigation by component
| Component | Navigation | Wrap? | Activation |
|---|---|---|---|
| Modal | Tab | Trap | Enter / Space |
| Menu | Arrow | Yes | Enter / Space |
| Tabs | Arrow | Yes | Auto or Enter |
| Toolbar | Arrow | Yes | Enter / Space |
| Combobox | Arrow | No | Enter |
| Tree | Arrow | No | Enter |
Required ARIA by component
| Component | Required attributes |
|---|---|
| Modal | role="dialog", aria-modal, aria-labelledby |
| Alert Dialog | role="alertdialog", aria-modal, aria-labelledby, aria-describedby |
| Menu | role="menu", aria-labelledby |
| Menu Item | role="menuitem" |
| Tabs | role="tablist", aria-orientation |
| Tab | role="tab", aria-selected, aria-controls |
| Tab Panel | role="tabpanel", aria-labelledby |
| Combobox | role="combobox", aria-haspopup, aria-expanded, aria-controls |
Decision guide
Building interactive component?
├── Is it an overlay?
│ ├── Modal / blocking? → Use focus trap + aria-modal
│ └── Non-modal? → No trap, but restore focus on close
├── Is it a list of options?
│ ├── Actions? → role="menu" + arrow keys + wrap
│ ├── Selection? → role="listbox" + arrow keys + no wrap
│ └── Navigation? → role="tablist" + arrow keys + wrap
├── Is it a button group?
│ └── Toolbar? → role="toolbar" + arrow keys + one tab stop
└── Is it page-level?
└── Add skip links (first focusable element)
Last reviewed by @jschuyler