Skip to main content
CodeRocket
Accessibilitycriticalkeyboard

Enable keyboard navigation for all elements

rule · keyboard-navigation

All interactive elements must be keyboard accessible with a logical focus order that matches the visual layout.

Code Example

HTML
<!-- ✅ 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

KeyExpected Action
TabMove to next focusable element
Shift+TabMove to previous focusable element
EnterActivate buttons/links
SpaceActivate buttons, toggle checkboxes
Arrow keysNavigate within widgets (menus, tabs, sliders)
EscapeClose modals, menus, dropdowns

Custom Interactive Elements

HTML
<!-- ❌ 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

TSX
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.

TSX
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