Eidetic

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.

Global
Navigation

Move focus to next focusable element
+
Move focus to previous focusable element
Activate button, link, or form submission
Activate button or toggle checkbox/switch
Esc
Close dialog, dropdown, or cancel action

Application
Quick Actions

+K
Open command palette / search
+/
Show keyboard shortcuts
+N
Create new item
+S
Save current item

Use 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 items
Home
Jump to first item
End
Jump to last item
Esc
Close without selection

Tabs

+
Switch tabs (horizontal)
+
Switch tabs (vertical)
Home
Activate first tab
End
Activate last tab

Dialogs & Modals

Cycle through elements (trapped)
Esc
Close and return focus
Confirm action (on confirm button)

Focus is trapped inside dialogs and restored when closed.

Sliders

+
Decrease by step
+
Increase by step
Home
Set to minimum
End
Set to maximum
Page Up
Large step increase
Page Down
Large step decrease

Focus 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