⌨️ Keyboard Navigation Patterns

Complete guide to implementing keyboard accessibility, ensuring all Claude Code features and generated code are fully operable without a mouse.

🎯 Overview

Keyboard navigation is fundamental to accessibility. Many users rely exclusively on keyboards due to motor disabilities, visual impairments, or personal preference. This guide covers implementing robust keyboard support in Claude Code interactions and ensuring AI-generated code follows keyboard accessibility best practices.

🔑 Core Keyboard Navigation Principles

1. Everything Must Be Keyboard Accessible

  • All interactive elements must be reachable via keyboard
  • No functionality should require a mouse
  • Keyboard shortcuts should have visual alternatives

2. Logical Tab Order

  • Follow visual reading order (left-to-right, top-to-bottom)
  • Group related controls
  • Skip repetitive navigation with skip links

3. Visible Focus Indicators

  • Never remove focus outlines without providing alternatives
  • Ensure 3:1 contrast ratio for focus indicators
  • Make focus indicators clearly visible

🛠️ Implementation Patterns

Basic Keyboard Navigation

// ✅ Good: Properly keyboard accessible component
class AccessibleComponent {
  constructor(element) {
    this.element = element;
    this.setupKeyboardHandling();
  }
  
  setupKeyboardHandling() {
    // Make element focusable if not natively focusable
    if (!this.element.matches('button, a, input, select, textarea')) {
      this.element.setAttribute('tabindex', '0');
    }
    
    // Add keyboard event handlers
    this.element.addEventListener('keydown', this.handleKeyDown.bind(this));
  }
  
  handleKeyDown(event) {
    switch (event.key) {
      case 'Enter':
      case ' ':
        // Activate on Enter or Space
        event.preventDefault();
        this.activate();
        break;
      case 'Escape':
        // Close or cancel
        this.close();
        break;
    }
  }
}

Focus Management

// Focus trap for modals and dialogs
class FocusTrap {
  constructor(container) {
    this.container = container;
    this.focusableElements = this.getFocusableElements();
    this.firstElement = this.focusableElements[0];
    this.lastElement = this.focusableElements[this.focusableElements.length - 1];
  }
  
  getFocusableElements() {
    const selector = `
      a[href], button:not([disabled]), textarea:not([disabled]),
      input:not([disabled]), select:not([disabled]),
      [tabindex]:not([tabindex="-1"])
    `;
    return Array.from(this.container.querySelectorAll(selector));
  }
  
  trapFocus(event) {
    if (event.key !== 'Tab') return;
    
    if (event.shiftKey) {
      // Shift + Tab
      if (document.activeElement === this.firstElement) {
        event.preventDefault();
        this.lastElement.focus();
      }
    } else {
      // Tab
      if (document.activeElement === this.lastElement) {
        event.preventDefault();
        this.firstElement.focus();
      }
    }
  }
  
  activate() {
    this.container.addEventListener('keydown', this.trapFocus.bind(this));
    this.firstElement?.focus();
  }
}
<!-- Skip to main content link -->
<a href="#main" class="skip-link">Skip to main content</a>
 
<style>
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 999;
}
 
.skip-link:focus {
  left: 0;
  background: #000;
  color: #fff;
  padding: 8px;
  text-decoration: none;
}
</style>

🎮 Advanced Keyboard Patterns

Roving Tabindex for Lists

// Implement arrow key navigation in lists
class RovingTabindex {
  constructor(container) {
    this.container = container;
    this.items = Array.from(container.querySelectorAll('[role="option"]'));
    this.currentIndex = 0;
    this.init();
  }
  
  init() {
    // Set initial tabindex
    this.items.forEach((item, index) => {
      item.setAttribute('tabindex', index === 0 ? '0' : '-1');
    });
    
    // Add event listeners
    this.container.addEventListener('keydown', this.handleKeydown.bind(this));
  }
  
  handleKeydown(event) {
    let handled = false;
    
    switch (event.key) {
      case 'ArrowDown':
        this.focusNext();
        handled = true;
        break;
      case 'ArrowUp':
        this.focusPrevious();
        handled = true;
        break;
      case 'Home':
        this.focusFirst();
        handled = true;
        break;
      case 'End':
        this.focusLast();
        handled = true;
        break;
    }
    
    if (handled) {
      event.preventDefault();
    }
  }
  
  focusNext() {
    this.changeFocus(this.currentIndex + 1);
  }
  
  focusPrevious() {
    this.changeFocus(this.currentIndex - 1);
  }
  
  changeFocus(newIndex) {
    // Remove tabindex from current item
    this.items[this.currentIndex].setAttribute('tabindex', '-1');
    
    // Wrap around
    if (newIndex < 0) newIndex = this.items.length - 1;
    if (newIndex >= this.items.length) newIndex = 0;
    
    // Update current index and focus
    this.currentIndex = newIndex;
    this.items[this.currentIndex].setAttribute('tabindex', '0');
    this.items[this.currentIndex].focus();
  }
}

Keyboard Shortcuts

// Global keyboard shortcuts manager
class KeyboardShortcuts {
  constructor() {
    this.shortcuts = new Map();
    this.init();
  }
  
  init() {
    document.addEventListener('keydown', this.handleKeydown.bind(this));
  }
  
  register(shortcut, callback, description) {
    this.shortcuts.set(this.normalizeShortcut(shortcut), {
      callback,
      description,
      shortcut
    });
  }
  
  normalizeShortcut(shortcut) {
    return shortcut
      .toLowerCase()
      .split('+')
      .sort()
      .join('+');
  }
  
  handleKeydown(event) {
    const keys = [];
    if (event.ctrlKey) keys.push('ctrl');
    if (event.altKey) keys.push('alt');
    if (event.shiftKey) keys.push('shift');
    if (event.metaKey) keys.push('meta');
    keys.push(event.key.toLowerCase());
    
    const shortcut = keys.join('+');
    const handler = this.shortcuts.get(shortcut);
    
    if (handler) {
      event.preventDefault();
      handler.callback(event);
    }
  }
  
  // Generate help dialog
  getHelp() {
    const shortcuts = Array.from(this.shortcuts.entries());
    return shortcuts.map(([key, {description, shortcut}]) => ({
      shortcut: shortcut,
      description: description
    }));
  }
}
 
// Usage
const shortcuts = new KeyboardShortcuts();
shortcuts.register('ctrl+s', () => saveFile(), 'Save current file');
shortcuts.register('ctrl+shift+p', () => openCommandPalette(), 'Open command palette');

🔍 Claude Code Terminal Keyboard Navigation

Essential Terminal Shortcuts

# Standard terminal navigation
Ctrl+A    # Beginning of line
Ctrl+E    # End of line
Ctrl+K    # Delete to end of line
Ctrl+U    # Delete to beginning of line
Ctrl+W    # Delete word backward
Alt+B     # Move backward one word
Alt+F     # Move forward one word
 
# Claude Code specific (proposed)
Ctrl+Space    # Trigger Claude Code suggestion
Ctrl+Enter    # Execute Claude Code command
Ctrl+Shift+C  # Copy Claude Code response
Ctrl+Shift+V  # Paste into Claude Code context

Making Claude Code Responses Navigable

// Structure Claude Code output for keyboard navigation
class NavigableResponse {
  constructor(response: string) {
    this.sections = this.parseResponse(response);
    this.currentSection = 0;
  }
  
  parseResponse(response: string): Section[] {
    // Split response into navigable sections
    return response.split(/^##/m).map(section => ({
      title: section.split('\n')[0].trim(),
      content: section.split('\n').slice(1).join('\n'),
      type: this.detectSectionType(section)
    }));
  }
  
  detectSectionType(section: string): SectionType {
    if (section.includes('```')) return 'code';
    if (section.match(/^\d+\./m)) return 'list';
    return 'text';
  }
  
  navigateNext() {
    if (this.currentSection < this.sections.length - 1) {
      this.currentSection++;
      this.announceSection();
    }
  }
  
  navigatePrevious() {
    if (this.currentSection > 0) {
      this.currentSection--;
      this.announceSection();
    }
  }
  
  announceSection() {
    const section = this.sections[this.currentSection];
    console.log(`\n=== ${section.title} (${this.currentSection + 1}/${this.sections.length}) ===`);
    console.log(section.content);
  }
}

📋 Testing Keyboard Navigation

Manual Testing Checklist

  • Tab Navigation

    • Can reach all interactive elements with Tab
    • Tab order follows logical reading order
    • Shift+Tab moves backward correctly
  • Focus Indicators

    • All focused elements have visible indicators
    • Focus indicators meet contrast requirements
    • Custom focus styles are as visible as defaults
  • Keyboard Activation

    • Buttons activate with Space and Enter
    • Links activate with Enter
    • Form controls work with expected keys
  • Arrow Key Navigation

    • Menus navigate with arrow keys
    • Tab panels use appropriate arrow keys
    • Lists support arrow navigation where expected
  • Escape Key

    • Closes modals and popups
    • Cancels operations
    • Returns focus appropriately
  • Keyboard Shortcuts

    • Don’t conflict with browser/OS shortcuts
    • Are discoverable (help menu)
    • Have non-keyboard alternatives

Automated Testing

// Playwright keyboard navigation test
import { test, expect } from '@playwright/test';
 
test('keyboard navigation', async ({ page }) => {
  await page.goto('/');
  
  // Test tab navigation
  await page.keyboard.press('Tab');
  await expect(page.locator(':focus')).toHaveAttribute('data-testid', 'skip-link');
  
  await page.keyboard.press('Tab');
  await expect(page.locator(':focus')).toHaveAttribute('data-testid', 'main-nav');
  
  // Test Enter activation
  await page.keyboard.press('Enter');
  await expect(page).toHaveURL('/navigation');
  
  // Test Escape key
  await page.keyboard.press('Control+K'); // Open command palette
  await expect(page.locator('[role="dialog"]')).toBeVisible();
  
  await page.keyboard.press('Escape');
  await expect(page.locator('[role="dialog"]')).not.toBeVisible();
});

🎨 AI-Generated Code Keyboard Patterns

Ensuring Generated Code is Keyboard Accessible

// Claude Code should generate keyboard-accessible components
const generateAccessibleButton = (text: string, onClick: string) => {
  return `
<button 
  type="button"
  onclick="${onClick}"
  onkeydown="if(event.key === 'Enter' || event.key === ' ') { event.preventDefault(); ${onClick} }"
>
  ${text}
</button>
  `.trim();
};
 
// Better: Use semantic HTML that's keyboard accessible by default
const generateSemanticButton = (text: string, onClick: string) => {
  return `<button type="button" onclick="${onClick}">${text}</button>`;
};

Common Keyboard Patterns to Include

// Template for keyboard-navigable component
const keyboardNavigableTemplate = `
class KeyboardNavigable {
  constructor(element) {
    this.element = element;
    this.focusableChildren = this.element.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    
    // Ensure element is focusable
    if (!this.element.hasAttribute('tabindex')) {
      this.element.setAttribute('tabindex', '0');
    }
    
    // Add ARIA attributes
    this.element.setAttribute('role', 'region');
    this.element.setAttribute('aria-label', 'Interactive component');
    
    // Setup keyboard handlers
    this.element.addEventListener('keydown', this.handleKeyDown.bind(this));
  }
  
  handleKeyDown(event) {
    // Handle common keyboard patterns
    switch(event.key) {
      case 'Tab':
        // Let default tab behavior work
        break;
      case 'Escape':
        this.close?.();
        break;
      case 'Enter':
      case ' ':
        if (event.target === this.element) {
          event.preventDefault();
          this.activate?.();
        }
        break;
    }
  }
}
`;

🚀 Best Practices

1. Focus Management

// Save and restore focus when showing/hiding content
class FocusManager {
  constructor() {
    this.previousFocus = null;
  }
  
  saveFocus() {
    this.previousFocus = document.activeElement;
  }
  
  restoreFocus() {
    if (this.previousFocus && this.previousFocus.focus) {
      this.previousFocus.focus();
    }
  }
  
  focusFirst(container) {
    const focusable = container.querySelector(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    focusable?.focus();
  }
}

2. Keyboard Shortcut Guidelines

  • Use standard shortcuts when possible
  • Avoid single-key shortcuts (except in specific contexts)
  • Provide customization options
  • Document all shortcuts
  • Test for conflicts

3. Visual Feedback

/* Enhanced focus indicators */
:focus {
  outline: 3px solid #0066cc;
  outline-offset: 2px;
}
 
/* High contrast mode support */
@media (prefers-contrast: high) {
  :focus {
    outline: 3px solid;
    outline-offset: 3px;
  }
}
 
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

📚 Resources

Standards and Guidelines

Testing Tools

🎯 Key Takeaways

  1. Everything must be keyboard accessible - No exceptions
  2. Follow platform conventions - Users expect standard behaviors
  3. Provide clear focus indicators - Never remove without replacement
  4. Test without a mouse - Unplug it during development
  5. Consider keyboard-only users - They’re more common than you think

🧭 Quick Navigation

← Screen Reader Guide | Voice Control → | Visual Accessibility →