home / skills / dasien / claudemultiagenttemplate / keyboard-navigation

keyboard-navigation skill

/templates/.claude/skills/keyboard-navigation

This skill helps you implement keyboard navigation and focus management to ensure accessible, trap-free interactions across UIs.

npx playbooks add skill dasien/claudemultiagenttemplate --skill keyboard-navigation

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
12.1 KB
---
name: "Keyboard Navigation"
description: "Implement focus management, optimize tab order, and validate keyboard shortcuts"
category: "accessibility"
required_tools: ["Read", "Bash"]
---

# Keyboard Navigation

## Purpose
Ensure all functionality is accessible via keyboard for users who cannot or do not use a mouse, including those with motor disabilities and power users.

## When to Use
- Building interactive UIs
- Implementing modals, dropdowns, menus
- Creating custom controls
- Accessibility testing
- Keyboard shortcut implementation

## Key Capabilities

1. **Focus Management** - Ensure logical focus order and visible indicators
2. **Keyboard Shortcuts** - Implement accessible shortcuts
3. **Focus Trapping** - Manage focus in modals and dialogs

## Approach

1. **Enable Keyboard Access**
   - All interactive elements focusable
   - Logical tab order (left-to-right, top-to-bottom)
   - No keyboard traps (can escape any control)
   - Custom controls keyboard accessible

2. **Implement Standard Keys**
   - **Tab**: Move forward through interactive elements
   - **Shift+Tab**: Move backward
   - **Enter**: Activate buttons, submit forms, follow links
   - **Space**: Activate buttons, checkboxes, toggle controls
   - **Escape**: Close modals, cancel operations, clear selections
   - **Arrow keys**: Navigate within components (menus, tabs, lists)
   - **Home/End**: Jump to start/end of content

3. **Provide Visible Focus Indicators**
   - Clear outline or ring
   - Sufficient contrast (3:1 minimum)
   - Consistent across site
   - Never remove without replacement

4. **Manage Focus**
   - Set focus to modals when opened
   - Restore focus when closed
   - Trap focus in modals
   - Skip navigation links

5. **Test Keyboard Navigation**
   - Unplug mouse
   - Navigate entire site with keyboard only
   - Verify all functionality accessible
   - Check focus indicators visible

## Example

**Context**: Accessible modal dialog with focus trap

```javascript
class AccessibleModal {
    constructor(modalElement) {
        this.modal = modalElement;
        this.focusableElements = null;
        this.firstFocusable = null;
        this.lastFocusable = null;
        this.previousFocus = null;
        
        // Bind event handlers
        this.handleKeyDown = this.handleKeyDown.bind(this);
    }
    
    open() {
        // Store current focus to restore later
        this.previousFocus = document.activeElement;
        
        // Show modal
        this.modal.style.display = 'block';
        this.modal.setAttribute('aria-hidden', 'false');
        
        // Get all focusable elements
        this.focusableElements = this.modal.querySelectorAll(
            'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        );
        
        this.firstFocusable = this.focusableElements[0];
        this.lastFocusable = this.focusableElements[this.focusableElements.length - 1];
        
        // Focus first element or modal itself
        if (this.firstFocusable) {
            this.firstFocusable.focus();
        } else {
            this.modal.focus();
        }
        
        // Trap focus
        this.modal.addEventListener('keydown', this.handleKeyDown);
        
        // Prevent body scroll
        document.body.style.overflow = 'hidden';
    }
    
    close() {
        // Hide modal
        this.modal.style.display = 'none';
        this.modal.setAttribute('aria-hidden', 'true');
        
        // Remove event listener
        this.modal.removeEventListener('keydown', this.handleKeyDown);
        
        // Restore focus to previous element
        if (this.previousFocus) {
            this.previousFocus.focus();
        }
        
        // Restore body scroll
        document.body.style.overflow = '';
    }
    
    handleKeyDown(e) {
        // Trap focus within modal
        if (e.key === 'Tab') {
            if (e.shiftKey) {
                // Shift+Tab: backward
                if (document.activeElement === this.firstFocusable) {
                    e.preventDefault();
                    this.lastFocusable.focus();
                }
            } else {
                // Tab: forward
                if (document.activeElement === this.lastFocusable) {
                    e.preventDefault();
                    this.firstFocusable.focus();
                }
            }
        }
        
        // Close on Escape
        if (e.key === 'Escape') {
            this.close();
        }
    }
}

// Usage
const modal = new AccessibleModal(document.getElementById('myModal'));

document.getElementById('openModal').addEventListener('click', () => {
    modal.open();
});

document.getElementById('closeModal').addEventListener('click', () => {
    modal.close();
});
```

**HTML for Accessible Modal**:
```html
<!-- Modal trigger -->
<button 
    id="openModal"
    aria-haspopup="dialog"
    aria-expanded="false"
>
    Open Dialog
</button>

<!-- Modal -->
<div 
    id="myModal"
    role="dialog"
    aria-modal="true"
    aria-labelledby="modal-title"
    aria-describedby="modal-description"
    aria-hidden="true"
    tabindex="-1"
>
    <div class="modal-content">
        <h2 id="modal-title">Confirm Action</h2>
        <p id="modal-description">
            Are you sure you want to delete this item?
        </p>
        
        <div class="modal-actions">
            <button id="confirmButton">
                Confirm
            </button>
            <button id="closeModal">
                Cancel
            </button>
        </div>
    </div>
</div>

<style>
    /* Visible focus indicators */
    button:focus,
    a:focus,
    input:focus {
        outline: 3px solid #0066cc;
        outline-offset: 2px;
    }
    
    /* Focus within modal */
    .modal-content *:focus {
        outline-color: #0066cc;
    }
</style>
```

**Accessible Dropdown Menu**:
```javascript
class AccessibleDropdown {
    constructor(button, menu) {
        this.button = button;
        this.menu = menu;
        this.menuItems = menu.querySelectorAll('[role="menuitem"]');
        this.currentIndex = -1;
        
        this.button.addEventListener('click', () => this.toggle());
        this.button.addEventListener('keydown', (e) => this.handleButtonKey(e));
        this.menu.addEventListener('keydown', (e) => this.handleMenuKey(e));
        
        // Close on outside click
        document.addEventListener('click', (e) => {
            if (!this.button.contains(e.target) && !this.menu.contains(e.target)) {
                this.close();
            }
        });
    }
    
    toggle() {
        if (this.menu.style.display === 'block') {
            this.close();
        } else {
            this.open();
        }
    }
    
    open() {
        this.menu.style.display = 'block';
        this.button.setAttribute('aria-expanded', 'true');
        this.currentIndex = 0;
        this.menuItems[0].focus();
    }
    
    close() {
        this.menu.style.display = 'none';
        this.button.setAttribute('aria-expanded', 'false');
        this.button.focus();
        this.currentIndex = -1;
    }
    
    handleButtonKey(e) {
        // Open on Enter, Space, or Down Arrow
        if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
            e.preventDefault();
            this.open();
        }
    }
    
    handleMenuKey(e) {
        switch (e.key) {
            case 'ArrowDown':
                e.preventDefault();
                this.currentIndex = (this.currentIndex + 1) % this.menuItems.length;
                this.menuItems[this.currentIndex].focus();
                break;
            
            case 'ArrowUp':
                e.preventDefault();
                this.currentIndex = this.currentIndex - 1;
                if (this.currentIndex < 0) {
                    this.currentIndex = this.menuItems.length - 1;
                }
                this.menuItems[this.currentIndex].focus();
                break;
            
            case 'Home':
                e.preventDefault();
                this.currentIndex = 0;
                this.menuItems[0].focus();
                break;
            
            case 'End':
                e.preventDefault();
                this.currentIndex = this.menuItems.length - 1;
                this.menuItems[this.currentIndex].focus();
                break;
            
            case 'Escape':
                e.preventDefault();
                this.close();
                break;
            
            case 'Enter':
            case ' ':
                e.preventDefault();
                this.menuItems[this.currentIndex].click();
                this.close();
                break;
        }
    }
}
```

**Skip Navigation Link**:
```html
<!-- Skip link (first focusable element) -->
<a href="#main-content" class="skip-link">
    Skip to main content
</a>

<!-- Navigation -->
<nav>
    <!-- Many navigation links -->
</nav>

<!-- Main content -->
<main id="main-content" tabindex="-1">
    <!-- Page content -->
</main>

<style>
    /* Hidden by default, visible on focus */
    .skip-link {
        position: absolute;
        top: -40px;
        left: 0;
        background: #000;
        color: #fff;
        padding: 8px;
        text-decoration: none;
        z-index: 100;
    }
    
    .skip-link:focus {
        top: 0;
    }
</style>
```

**Keyboard Shortcuts**:
```javascript
// Global keyboard shortcuts
document.addEventListener('keydown', (e) => {
    // Ignore if typing in input
    if (e.target.matches('input, textarea')) {
        return;
    }
    
    // Ctrl/Cmd + K: Search
    if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
        e.preventDefault();
        document.getElementById('search-input').focus();
    }
    
    // ? : Show keyboard shortcuts
    if (e.key === '?') {
        e.preventDefault();
        showKeyboardShortcuts();
    }
    
    // Esc: Close any open modals
    if (e.key === 'Escape') {
        closeAllModals();
    }
});

// Show keyboard shortcuts dialog
function showKeyboardShortcuts() {
    const shortcuts = [
        { keys: 'Ctrl+K', action: 'Open search' },
        { keys: '?', action: 'Show keyboard shortcuts' },
        { keys: 'Esc', action: 'Close dialog' },
        { keys: 'Tab', action: 'Next element' },
        { keys: 'Shift+Tab', action: 'Previous element' },
    ];
    
    // Display shortcuts in accessible modal
    showModal({
        title: 'Keyboard Shortcuts',
        content: renderShortcuts(shortcuts)
    });
}
```

**Testing Keyboard Navigation**:
```markdown
## Testing Checklist

### Basic Navigation
- [ ] Tab moves forward through all interactive elements
- [ ] Shift+Tab moves backward
- [ ] Focus indicator always visible
- [ ] Tab order is logical (reading order)
- [ ] No keyboard traps (can escape every control)

### Interactive Elements
- [ ] Enter activates buttons and links
- [ ] Space activates buttons and checkboxes
- [ ] Arrow keys navigate dropdowns/menus
- [ ] Escape closes modals and menus
- [ ] Custom controls have keyboard support

### Forms
- [ ] Tab moves between form fields
- [ ] Arrow keys work in radio groups
- [ ] Space toggles checkboxes
- [ ] Enter submits form
- [ ] Error messages reachable via keyboard

### Modals
- [ ] Focus moves to modal when opened
- [ ] Tab cycles within modal (focus trap)
- [ ] Escape closes modal
- [ ] Focus returns to trigger on close

### Navigation
- [ ] Skip link works
- [ ] All navigation items reachable
- [ ] Current page indicated
- [ ] Submenus keyboard accessible

### Custom Controls
- [ ] Appropriate ARIA roles
- [ ] Keyboard interactions documented
- [ ] All states keyboard accessible
- [ ] Focus management correct
```

## Best Practices

- ✅ All interactive elements keyboard accessible
- ✅ Visible focus indicators (3px outline minimum)
- ✅ Logical tab order (no tabindex > 0)
- ✅ Escape closes modals/menus
- ✅ Arrow keys for menus/lists/tabs
- ✅ Home/End for start/end navigation
- ✅ Skip navigation links
- ✅ Focus trapping in modals
- ✅ Restore focus after closing modals
- ✅ Document keyboard shortcuts
- ❌ Avoid: Keyboard traps (can't escape)
- ❌ Avoid: Removing focus outline without replacement
- ❌ Avoid: Using tabindex > 0 (breaks natural order)
- ❌ Avoid: Non-standard keyboard interactions

Overview

This skill provides patterns and checks to implement robust keyboard navigation across interactive web apps. It focuses on focus management, logical tab order, visible focus indicators, and accessible keyboard shortcuts so all features are reachable without a mouse.

How this skill works

The skill inspects interactive elements and enforces keyboard-accessible behavior: making elements focusable, applying standard key mappings (Tab, Shift+Tab, Enter, Space, Escape, Arrow, Home/End), and implementing focus traps for modal dialogs. It also provides examples and a testing checklist to validate behavior and restore focus after dialogs close.

When to use it

  • Building interactive user interfaces (forms, controls, menus)
  • Implementing modals, dialogs, dropdowns, and popovers
  • Creating custom controls that need keyboard bindings
  • Adding or documenting global keyboard shortcuts
  • Performing accessibility testing and QA

Best practices

  • Ensure every interactive element is keyboard-focusable and follows reading order
  • Use visible focus indicators with sufficient contrast (at least 3:1) and never remove outlines without replacement
  • Avoid tabindex > 0; rely on logical DOM order and tabindex=-1 for programmatic focus
  • Set focus into modals when opened, trap focus inside, and restore focus to the trigger on close
  • Document global shortcuts and ignore them while typing in inputs to avoid collisions
  • Test by navigating the entire UI with the keyboard only and verify no keyboard traps

Example use cases

  • Accessible modal dialog that traps focus, closes on Escape, and returns focus to the opener
  • Dropdown menu navigable with Arrow keys, Home/End, Enter to select, and Escape to close
  • Global keyboard shortcut (Ctrl/Cmd+K) to focus search input, with '?' opening a shortcuts dialog
  • Skip-to-content link for keyboard and screen-reader users to bypass long navigation
  • Form interactions where Enter submits, Space toggles checkboxes, and Arrow keys move between radio options

FAQ

How do I prevent keyboard traps in custom widgets?

Ensure every widget includes a clear exit path (Escape to close, Tab/Shift+Tab to move out) and avoid disabling natural tab behavior; use tabindex=-1 only to receive programmatic focus.

When should I use tabindex=-1 versus tabindex=0?

Use tabindex=0 to make non-interactive elements reachable in natural order. Use tabindex=-1 to allow programmatic focus without adding the element to the tab order.