Enable keyboard navigation for all elements
All interactive elements are accessible via keyboard with logical focus order and hidden elements excluded from tab sequence.
- All interactive elements must be reachable via Tab key
- Focus order should follow visual reading order (left-to-right, top-to-bottom)
- Never use positive tabindex values—they break natural flow
- Hidden content must be removed from tab sequence with tabindex='-1'
- Keyboard traps are only acceptable inside active modals with a working Escape path
Rule Details
All interactive elements must be keyboard accessible with a logical focus order that matches the visual layout.
Code Example
<!-- ✅ Good: Natural tab order -->
<button>First</button>
<button>Second</button>
<button>Third</button>
<!-- ❌ Bad: Positive tabindex breaks flow -->
<button tabindex="3">First</button>
<button tabindex="1">Second</button>
<button tabindex="2">Third</button>
<!-- ✅ Remove from tab order (for hidden content) -->
<button tabindex="-1">Hidden from tab</button>Why It Matters
15% of users rely on keyboard navigation—broken focus order or keyboard traps make your site completely unusable for them.
Keyboard Navigation Basics
| Key | Expected Action |
|---|---|
| Tab | Move to next focusable element |
| Shift+Tab | Move to previous focusable element |
| Enter | Activate buttons/links |
| Space | Activate buttons, toggle checkboxes |
| Arrow keys | Navigate within widgets (menus, tabs, sliders) |
| Escape | Close modals, menus, dropdowns |
Custom Interactive Elements
<!-- ❌ Bad: div is not keyboard accessible -->
<div class="button" onclick="doSomething()">Click me</div>
<!-- ✅ Good: Native button is keyboard accessible -->
<button onclick="doSomething()">Click me</button>
<!-- ✅ Acceptable: Custom element with keyboard support -->
<div
role="button"
tabindex="0"
onclick="doSomething()"
onkeydown="if(event.key === 'Enter' || event.key === ' ') doSomething()"
>
Click me
</div>Hidden Content Management
function Modal({ isOpen, children }) {
return (
<div
role="dialog"
aria-modal="true"
// Remove from tab order when closed
tabIndex={isOpen ? 0 : -1}
style={{ display: isOpen ? 'block' : 'none' }}
>
{children}
</div>
)
}Focus Trapping for Modals
Focus trapping is valid only when the user is inside an active modal or popover that must temporarily contain interaction. Trapping focus in drawers, carousels, chat widgets, or sticky banners without a clear exit creates a keyboard trap.
import { useEffect, useRef } from 'react'
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!isOpen) return
const focusableElements = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const firstElement = focusableElements?.[0] as HTMLElement
const lastElement = focusableElements?.[focusableElements.length - 1] as HTMLElement
firstElement?.focus()
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault()
lastElement?.focus()
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault()
firstElement?.focus()
}
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [isOpen, onClose])
return <div ref={modalRef} role="dialog" aria-modal="true">{children}</div>
}Exceptions
- Temporary or intentionally inert UI can be removed from the focus order, but only when the same state is also communicated clearly to assistive technology users.
- A focus-management issue should be evaluated in the rendered interaction, not only from static markup, because route changes, overlays, and JS timing can change the real behavior.
- If a component is both unlabeled and focus-broken, fix the stronger user-facing orientation problem first rather than reporting multiple secondary symptoms.
Standards
- Align the implementation with W3C WAI: WCAG Overview and verify the rendered experience, not only the source code.
- Align the implementation with MDN: Accessibility and verify the rendered experience, not only the source code.
Verification
Automated Checks
- Use browser accessibility tooling, axe, Lighthouse, or equivalent automated checks against a representative rendered state.
Manual Checks
- Unplug your mouse and navigate the entire page using only keyboard
- Verify Tab moves through all interactive elements in visual order
- Confirm Enter/Space activate buttons and links
- Check that hidden elements cannot receive focus
- Test modals trap focus and Escape closes them
- Enter and exit every overlay, drawer, and widget to confirm focus is never trapped outside an active modal dialog
Use with AI
Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.
Check
Verify implementation
Test that all interactive elements are keyboard accessible, focus order matches visual/reading order, hidden content is not focusable, and no keyboard traps exist except in modals with proper escape handling.
Fix
Auto-fix issues
Ensure tab order follows DOM order matching visual layout. Use tabindex='-1' to remove hidden elements from focus. Avoid positive tabindex values. Ensure off-screen or display:none content cannot receive focus.
Explain
Learn more
Explain how logical focus order helps keyboard users navigate efficiently, why hidden elements must be removed from the tab sequence to avoid confusion, and how focus management affects users of screen readers and switch devices.
Review
Code review
Review the rendered markup and interactive states that affect Enable keyboard navigation for all elements. Flag exact elements, roles, labels, focus behavior, or keyboard interactions that violate the rule, and note how to verify the fix with browser accessibility tooling or assistive tech.