Keyboard Navigation
Comprehensive guide to keyboard interactions, focus management, and shortcuts for accessible navigation.
Full Support
All components work without a mouse
Visible Focus
Clear focus indicators on all elements
Logical Order
Tab order follows visual layout
WAI-ARIA
Follows ARIA authoring practices
Global Keyboard Shortcuts
Shortcuts that work across all pages and components.
GlobalNavigation
⇥
Move focus to next focusable element⇧+⇥
Move focus to previous focusable elementActivate button, link, or form submission
Activate button or toggle checkbox/switch
Esc
Close dialog, dropdown, or cancel actionApplicationQuick Actions
⌘+K
Open command palette / search⌘+/
Show keyboard shortcuts⌘+N
Create new item⌘+S
Save current itemUse Ctrl instead of ⌘ on Windows/Linux
Component Keyboard Patterns
Standard keyboard interactions for common UI patterns.
Buttons
Activate button
Activate button
Checkboxes & Switches
Toggle checked state
Dropdowns & Menus
Open dropdown / select item
Open dropdown / select item
+
Navigate menu itemsHome
Jump to first itemEnd
Jump to last itemEsc
Close without selectionTabs
+
Switch tabs (horizontal)+
Switch tabs (vertical)Home
Activate first tabEnd
Activate last tabDialogs & Modals
⇥
Cycle through elements (trapped)Esc
Close and return focusConfirm action (on confirm button)
Focus is trapped inside dialogs and restored when closed.
Sliders
+
Decrease by step+
Increase by stepHome
Set to minimumEnd
Set to maximumPage Up
Large step increasePage Down
Large step decreaseFocus Management
How focus is handled throughout the application.
- Focus Trap: Dialogs, modals, and sheets trap focus within their boundaries. Users cannot Tab outside.
- Focus Restoration: When overlays close, focus returns to the element that triggered them.
- Focus Visible: All interactive elements show a visible focus ring (2px indigo ring with offset).
- Skip Links: Hidden "Skip to content" links appear on focus, allowing users to bypass navigation.
- Logical Tab Order: Focus order follows the visual layout. We never use positive tabindex values.
- Dynamic Content: When content is added/removed, focus is preserved or moved to a sensible location.
Focus Ring Styles
/* Default focus ring */.focus-ring { @apply focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-slate-950;}
/* Focus visible only (keyboard navigation) */.focus-visible-ring { @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2;}
/* Inset focus (for filled buttons) */.focus-ring-inset { @apply focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-inset;}Implementation Guidelines
How to add keyboard support to custom components.
Custom Interactive Element
function CustomButton({ onClick, children }) { const handleKeyDown = (e: React.KeyboardEvent) => { // Activate on Enter or Space if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() onClick?.() } }
return ( <div role="button" tabIndex={0} onClick={onClick} onKeyDown={handleKeyDown} className="focus-visible:ring-2 focus-visible:ring-indigo-500" > {children} </div> )}Arrow Key Navigation (Roving Tabindex)
function TabList({ tabs, activeTab, onChange }) { const handleKeyDown = (e: React.KeyboardEvent, index: number) => { let nextIndex = index
switch (e.key) { case 'ArrowRight': nextIndex = (index + 1) % tabs.length break case 'ArrowLeft': nextIndex = (index - 1 + tabs.length) % tabs.length break case 'Home': nextIndex = 0 break case 'End': nextIndex = tabs.length - 1 break default: return }
e.preventDefault() onChange(tabs[nextIndex].id) // Focus the new tab document.getElementById(`tab-${tabs[nextIndex].id}`)?.focus() }
return ( <div role="tablist"> {tabs.map((tab, index) => ( <button key={tab.id} id={`tab-${tab.id}`} role="tab" aria-selected={activeTab === tab.id} tabIndex={activeTab === tab.id ? 0 : -1} onKeyDown={(e) => handleKeyDown(e, index)} > {tab.label} </button> ))} </div> )}Focus Trap for Dialogs
function useFocusTrap(ref: RefObject<HTMLElement>, isActive: boolean) { useEffect(() => { if (!isActive || !ref.current) return
const focusableElements = ref.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
const handleKeyDown = (e: KeyboardEvent) => { if (e.key !== 'Tab') return
if (e.shiftKey && document.activeElement === firstElement) { e.preventDefault() lastElement.focus() } else if (!e.shiftKey && document.activeElement === lastElement) { e.preventDefault() firstElement.focus() } }
// Focus first element firstElement?.focus()
document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [ref, isActive])}Testing Keyboard Accessibility
How to verify keyboard support is working correctly.
Manual Testing Checklist
- 1Tab through the entire page - all interactive elements should be reachable
- 2Focus indicator is visible on every focused element
- 3Tab order matches visual layout (left to right, top to bottom)
- 4Buttons activate with Enter and Space
- 5Dropdowns open and navigate with arrow keys
- 6Escape closes dialogs/dropdowns and restores focus
- 7Focus doesn't get trapped in unexpected places
- 8Modal focus is trapped inside and restored on close
Keyboard Support Quick Reference
Essential Keys
- Tab — Move focus forward
- Shift+Tab — Move focus backward
- Enter — Activate element
- Space — Activate / toggle
- Escape — Close / cancel
Navigation Keys
- Arrow keys — Navigate within component
- Home — Go to first item
- End — Go to last item
- Page Up/Down — Large jumps
Focus Rules
- Visible focus indicator always
- Logical tab order
- Trap focus in modals
- Restore focus on close
- No positive tabindex