⌨️ 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 Links
<!-- 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 contextMaking 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
- WCAG 2.1 Keyboard Guidelines
- ARIA Authoring Practices - Keyboard Navigation
- WAI Keyboard Compatibility
Testing Tools
🎯 Key Takeaways
- Everything must be keyboard accessible - No exceptions
- Follow platform conventions - Users expect standard behaviors
- Provide clear focus indicators - Never remove without replacement
- Test without a mouse - Unplug it during development
- Consider keyboard-only users - They’re more common than you think
🧭 Quick Navigation
← Screen Reader Guide | Voice Control → | Visual Accessibility →